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": {
|
"connectionName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"agentId": {
|
"metadata": {
|
||||||
"type": "string"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"os": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lettaCodeVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nodeVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"workingDirectory": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gitBranch": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["deviceId", "connectionName"]
|
"required": ["deviceId", "connectionName"]
|
||||||
@@ -26342,7 +26360,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "agentId",
|
"name": "onlineOnly",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -26363,9 +26381,13 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"connectionId": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"connectionId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"deviceId": {
|
"deviceId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -26381,16 +26403,22 @@
|
|||||||
"apiKeyOwner": {
|
"apiKeyOwner": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"agentId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"podId": {
|
"podId": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
},
|
},
|
||||||
"connectedAt": {
|
"connectedAt": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
},
|
},
|
||||||
"lastHeartbeat": {
|
"lastHeartbeat": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"lastSeenAt": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"firstSeenAt": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"currentMode": {
|
"currentMode": {
|
||||||
@@ -26401,16 +26429,40 @@
|
|||||||
"plan",
|
"plan",
|
||||||
"bypassPermissions"
|
"bypassPermissions"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"os": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lettaCodeVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nodeVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"workingDirectory": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gitBranch": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"id",
|
||||||
"connectionId",
|
"connectionId",
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"connectionName",
|
"connectionName",
|
||||||
"organizationId",
|
"organizationId",
|
||||||
"podId",
|
"podId",
|
||||||
"connectedAt",
|
"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": {
|
"components": {
|
||||||
|
|||||||
Reference in New Issue
Block a user