Documentation Index
Fetch the complete documentation index at: https://cloud-docs.mentra.glass/llms.txt
Use this file to discover all available pages before exploring further.
Overview
AppManager is responsible for the complete lifecycle of third-party apps within a user session. It handles app starting, stopping, connection management, webhook triggering, and state tracking. This manager encapsulates all app-related functionality that was previously scattered throughout the codebase.
File: packages/cloud/src/services/session/AppManager.ts
Key Responsibilities
- App Lifecycle Management: Starting, stopping, and restarting apps
- Connection Management: WebSocket connections and heartbeat monitoring
- State Tracking: Connection states, pending operations, and app status
- Webhook Communication: Triggering app webhooks for session initialization
- Hardware Compatibility: Checking app requirements against device capabilities
- Grace Period Handling: Managing reconnections during network interruptions
State Management
Connection States
enum AppConnectionState {
RUNNING = "running", // Active WebSocket connection
GRACE_PERIOD = "grace_period", // Waiting for natural reconnection (5s)
RESURRECTING = "resurrecting", // System actively restarting app
STOPPING = "stopping", // User/system initiated stop in progress
DISCONNECTED = "disconnected" // Available for resurrection
}
Internal State
class AppManager {
// Pending app start operations
private pendingConnections = new Map<string, PendingConnection>();
// Connection states for Apps
private connectionStates = new Map<string, AppConnectionState>();
// Heartbeat intervals for App connections
private heartbeatIntervals = new Map<string, NodeJS.Timeout>();
// App start times for session duration calculation
private appStartTimes = new Map<string, number>();
}
App Starting Process
Start Flow
Implementation
async startApp(packageName: string): Promise<AppStartResult> {
// Check if already running
if (this.isAppRunning(packageName)) {
return { success: true };
}
// Get app details
const app = await appService.getApp(packageName);
// Check hardware compatibility
if (this.userSession.capabilities) {
const compatibility = HardwareCompatibilityService.checkCompatibility(
app,
this.userSession.capabilities
);
if (!compatibility.isCompatible) {
return {
success: false,
error: {
stage: "HARDWARE_CHECK",
message: `Missing required hardware: ${compatibility.missingRequired.join(", ")}`
}
};
}
}
// Create promise for connection tracking
return new Promise((resolve, reject) => {
const startTime = Date.now();
// Set timeout for connection
const timeout = setTimeout(async () => {
// Handle timeout...
resolve({
success: false,
error: {
stage: "TIMEOUT",
message: `Connection timeout after ${APP_SESSION_TIMEOUT_MS}ms`
}
});
}, APP_SESSION_TIMEOUT_MS);
// Store pending connection
this.pendingConnections.set(packageName, {
packageName,
resolve,
reject,
timeout,
startTime
});
// Trigger webhook
this.triggerAppWebhookInternal(app, resolve, reject, startTime);
});
}
Webhook Triggering
private async triggerAppWebhookInternal(
app: AppI,
resolve: (result: AppStartResult) => void,
reject: (error: Error) => void,
startTime: number
): Promise<void> {
// Determine WebSocket URL based on environment
let augmentOSWebsocketUrl = "";
if (app.isSystemApp) {
// Internal URLs for system apps
if (process.env.PORTER_APP_NAME) {
// Kubernetes environment
augmentOSWebsocketUrl = `ws://${process.env.PORTER_APP_NAME}-cloud.default.svc.cluster.local:80/app-ws`;
} else if (process.env.CONTAINER_ENVIRONMENT === "true") {
// Docker Compose
augmentOSWebsocketUrl = "ws://cloud/app-ws";
} else {
// Local development
augmentOSWebsocketUrl = "ws://localhost:8002/app-ws";
}
} else {
// Public URL for third-party apps
augmentOSWebsocketUrl = `wss://${CLOUD_PUBLIC_HOST_NAME}/app-ws`;
}
// Trigger boot screen
this.userSession.displayManager.handleAppStart(app.packageName);
// Send webhook
await this.triggerWebhook(`${app.publicUrl}/webhook`, {
type: WebhookRequestType.SESSION_REQUEST,
sessionId: `${this.userSession.userId}-${app.packageName}`,
userId: this.userSession.userId,
timestamp: new Date().toISOString(),
augmentOSWebsocketUrl
});
}
Connection Handling
App Initialization
When an app connects via WebSocket:
async handleAppInit(ws: WebSocket, initMessage: AppConnectionInit): Promise<void> {
const { packageName, apiKey, sessionId } = initMessage;
// Validate API key
const isValidApiKey = await developerService.validateApiKey(
packageName,
apiKey,
this.userSession
);
if (!isValidApiKey) {
this.resolvePendingConnectionWithError(
packageName,
"AUTHENTICATION",
"Invalid API key"
);
ws.close(1008, "Invalid API key");
return;
}
// Check app is in loading state
if (!this.userSession.loadingApps.has(packageName) &&
!this.userSession.runningApps.has(packageName)) {
ws.close(1008, "App not started for this session");
return;
}
// Store WebSocket connection
this.userSession.appWebsockets.set(packageName, ws);
// Set up close handler
ws.on("close", (code, reason) => {
this.handleAppConnectionClosed(packageName, code, reason.toString());
});
// Set up heartbeat
this.setupAppHeartbeat(packageName, ws);
// Update state
this.setAppConnectionState(packageName, AppConnectionState.RUNNING);
this.userSession.runningApps.add(packageName);
this.userSession.loadingApps.delete(packageName);
// Send connection acknowledgment
const connectionAck = {
type: CloudToAppMessageType.CONNECTION_ACK,
sessionId: `${this.userSession.userId}-${packageName}`,
userId: this.userSession.userId,
capabilities: this.userSession.capabilities,
timestamp: new Date()
};
ws.send(JSON.stringify(connectionAck));
// Resolve pending connection
this.resolvePendingConnection(packageName);
}
Heartbeat System
Maintains connection health:
private setupAppHeartbeat(packageName: string, ws: WebSocket): void {
const HEARTBEAT_INTERVAL = 10000; // 10 seconds
// Clear any existing heartbeat
this.clearAppHeartbeat(packageName);
// Set up new heartbeat
const heartbeatInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
} else {
this.clearAppHeartbeat(packageName);
}
}, HEARTBEAT_INTERVAL);
this.heartbeatIntervals.set(packageName, heartbeatInterval);
// Set up pong handler
ws.on("pong", () => {
// Connection still alive
});
}
App Stopping
Stop Process
async stopApp(packageName: string): Promise<void> {
if (!this.isAppRunning(packageName)) {
return;
}
// Set state to STOPPING
this.setAppConnectionState(packageName, AppConnectionState.STOPPING);
// Remove from running apps
this.userSession.runningApps.delete(packageName);
// Clear subscriptions
subscriptionService.removeSubscriptions(
this.userSession.userId,
packageName
);
// Send stop message and close connection
const appWebsocket = this.userSession.appWebsockets.get(packageName);
if (appWebsocket?.readyState === WebSocket.OPEN) {
const message = {
type: CloudToAppMessageType.APP_STOPPED,
reason: "USER_REQUEST",
timestamp: new Date()
};
appWebsocket.send(JSON.stringify(message));
appWebsocket.close(1000, "App stopped");
}
// Update database
const user = await User.findByEmail(this.userSession.userId);
await user?.removeRunningApp(packageName);
// Clean up connections
this.userSession.appWebsockets.delete(packageName);
// Clean up display and dashboard
this.userSession.displayManager.handleAppStop(packageName);
this.userSession.dashboardManager.cleanupAppContent(packageName);
// Track metrics
const startTime = this.appStartTimes.get(packageName);
if (startTime) {
const sessionDuration = Date.now() - startTime;
await PosthogService.trackEvent("app_stop", this.userSession.userId, {
packageName,
sessionDuration
});
}
}
Grace Period Handling
When an app disconnects unexpectedly:
private handleAppConnectionClosed(
packageName: string,
code: number,
reason: string
): void {
const currentState = this.getAppConnectionState(packageName);
// If intentionally stopping, clean up immediately
if (currentState === AppConnectionState.STOPPING) {
this.cleanupAppConnection(packageName);
return;
}
// Start grace period
this.setAppConnectionState(packageName, AppConnectionState.GRACE_PERIOD);
// Set reconnection timer (5 seconds)
const timer = setTimeout(() => {
const state = this.getAppConnectionState(packageName);
if (state === AppConnectionState.GRACE_PERIOD) {
// Grace period expired, clean up
this.cleanupAppConnection(packageName);
}
}, 5000);
this.userSession._reconnectionTimers.set(packageName, timer);
}
Message Handling
Send Message to App
async sendMessageToApp(
packageName: string,
message: any
): Promise<AppMessageResult> {
const ws = this.userSession.appWebsockets.get(packageName);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
return { sent: true, resurrectionTriggered: false };
}
// Try resurrection if disconnected
const state = this.getAppConnectionState(packageName);
if (state === AppConnectionState.DISCONNECTED) {
const startResult = await this.startApp(packageName);
return {
sent: false,
resurrectionTriggered: true,
error: startResult.success ? undefined : "Failed to resurrect app"
};
}
return {
sent: false,
resurrectionTriggered: false,
error: `App in ${state} state`
};
}
Cleanup
Disposal
dispose(): void {
// Stop all running apps
for (const packageName of this.userSession.runningApps) {
this.stopApp(packageName).catch(error => {
this.logger.error(`Error stopping app ${packageName} during disposal:`, error);
});
}
// Clear all heartbeats
for (const [packageName, interval] of this.heartbeatIntervals) {
clearInterval(interval);
}
this.heartbeatIntervals.clear();
// Clear pending connections
for (const [packageName, pending] of this.pendingConnections) {
clearTimeout(pending.timeout);
pending.reject(new Error("Session disposed"));
}
this.pendingConnections.clear();
// Clear reconnection timers
for (const timer of this.userSession._reconnectionTimers.values()) {
clearTimeout(timer);
}
this.userSession._reconnectionTimers.clear();
}
Environment Configuration
AppManager adapts WebSocket URLs based on environment:
- Local Development:
ws://localhost:8002/app-ws
- Docker Compose:
ws://cloud/app-ws (for system apps)
- Kubernetes (Porter):
ws://${APP_NAME}-cloud.default.svc.cluster.local:80/app-ws
- Production:
wss://${CLOUD_PUBLIC_HOST_NAME}/app-ws
Best Practices
- Always check hardware compatibility before starting apps
- Handle connection timeouts to prevent hanging operations
- Implement grace periods for network interruptions
- Track app metrics for usage analytics
- Clean up resources when apps stop
- Use appropriate URLs based on environment and app type