From 357a3ad15b344be71756f6c397d2ceba1a739ab2 Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Wed, 25 Feb 2026 17:58:12 -0800 Subject: [PATCH] Shub/let 7721 make env permanent [LET-7721] (#9683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: env permanent * chore: env permanent * feat: add persistent environments with hybrid DB + Redis storage [LET-7721] Implements persistent storage for letta-code listener connections (environments) with hybrid PostgreSQL + Redis architecture: **Database Layer:** - Add `environments` table with device tracking, connection metadata, soft deletes - Store userId/apiKeyOwner, connection history (firstSeenAt, lastSeenAt) - Unique constraint on (organizationId, deviceId) - one environment per device per org - Auto-undelete previously deleted environments on reconnect **API Layer:** - Update environmentsContract with new fields (id, firstSeenAt, lastSeenAt, metadata) - Add deleteEnvironment endpoint (soft delete, closes WebSocket if online) - Add onlineOnly filter to listConnections for efficient online-only queries - Export ListConnectionsResponse type for proper client typing **Router Implementation:** - register(): Create/update DB environment, generate ephemeral connectionId - listConnections(): Hybrid query strategy (DB-first for all, Redis-first for onlineOnly) - deleteEnvironment(): Soft delete with Redis Pub/Sub for graceful WebSocket close - Filter by connectionId in DB using inArray() for onlineOnly performance **WebSocket Handler:** - Moved from apps/cloud-api to libs/utils-server for reusability - Update DB on connect/disconnect only (not heartbeat) - minimal write load - Store currentPodId and userId/apiKeyOwner on connect - Clear currentConnectionId/currentPodId on disconnect/error **Shared Types:** - Add EnvironmentMetadata interface in libs/types for cross-layer consistency - Update Redis schema to include currentMode field **UI Components:** - Add DeleteDeviceModal with offline-only restriction - Update DeviceSelector with delete button on hover for offline devices - Proper cache updates using ListConnectionsResponse type - Add translations for delete modal 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * docs: update letta remote setup instructions [LET-7721] Update local setup guide with clearer instructions: - Remove hardcoded ngrok URL requirement (ngrok generates URL automatically) - Update env var to use CLOUD_API_ENDPOINT_OVERRIDE - Add proper API key and base URL format - Include alternative setup using letta-code repo with bun dev 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * chore: fix env * fix: lint errors and make migration idempotent [LET-7721] - Remove unused imports (HiddenOnMobile, VisibleOnMobile, MiddleTruncate) - Fix type imports (use `import type` for type-only imports) - Remove non-null assertions in environmentsRouter (use safe null checks + filter) - Make migration idempotent with IF NOT EXISTS for table, indexes, and constraints - Use DO $$ block for foreign key constraint (handles duplicate_object exception) 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * chore: fix env --------- Co-authored-by: Letta --- fern/openapi.json | 161 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 10 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index 196fb9bc..8097aa37 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -26263,8 +26263,26 @@ "connectionName": { "type": "string" }, - "agentId": { - "type": "string" + "metadata": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "lettaCodeVersion": { + "type": "string" + }, + "nodeVersion": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "gitBranch": { + "type": "string" + } + }, + "additionalProperties": true } }, "required": ["deviceId", "connectionName"] @@ -26342,7 +26360,7 @@ } }, { - "name": "agentId", + "name": "onlineOnly", "in": "query", "schema": { "type": "string" @@ -26363,9 +26381,13 @@ "items": { "type": "object", "properties": { - "connectionId": { + "id": { "type": "string" }, + "connectionId": { + "type": "string", + "nullable": true + }, "deviceId": { "type": "string" }, @@ -26381,16 +26403,22 @@ "apiKeyOwner": { "type": "string" }, - "agentId": { - "type": "string" - }, "podId": { - "type": "string" + "type": "string", + "nullable": true }, "connectedAt": { - "type": "number" + "type": "number", + "nullable": true }, "lastHeartbeat": { + "type": "number", + "nullable": true + }, + "lastSeenAt": { + "type": "number" + }, + "firstSeenAt": { "type": "number" }, "currentMode": { @@ -26401,16 +26429,40 @@ "plan", "bypassPermissions" ] + }, + "metadata": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "lettaCodeVersion": { + "type": "string" + }, + "nodeVersion": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "gitBranch": { + "type": "string" + } + }, + "additionalProperties": true } }, "required": [ + "id", "connectionId", "deviceId", "connectionName", "organizationId", "podId", "connectedAt", - "lastHeartbeat" + "lastHeartbeat", + "lastSeenAt", + "firstSeenAt" ] } }, @@ -26743,6 +26795,95 @@ } } } + }, + "/v1/environments/{id}": { + "delete": { + "description": "Removes environment from list of environments", + "summary": "Delete Environment", + "tags": ["environments"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "environments.deleteEnvironment", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } } }, "components": {