Shub/let 7721 make env permanent [LET-7721] (#9683)
* 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 <noreply@letta.com> * 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 <noreply@letta.com> * 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 <noreply@letta.com> * chore: fix env --------- Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
committed by
Caren Thomas
parent
46971414a4
commit
357a3ad15b
@@ -26263,8 +26263,26 @@
|
||||
"connectionName": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentId": {
|
||||
"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": {
|
||||
|
||||
Reference in New Issue
Block a user