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