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:
Shubham Naik
2026-02-25 17:58:12 -08:00
committed by Caren Thomas
parent 46971414a4
commit 357a3ad15b

View File

@@ -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": {