feat: add metadata version, agent state, version, add netadata change
This commit is contained in:
parent
51f3656554
commit
0ccdef14ff
178
CLAUDE.md
Normal file
178
CLAUDE.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Handy Server - Development Guidelines
|
||||||
|
|
||||||
|
This document contains the development guidelines and instructions for the Handy Server project. This guide OVERRIDES any default behaviors and MUST be followed exactly.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Name**: handy-server
|
||||||
|
**Repository**: https://github.com/ex3ndr/handy-server.git
|
||||||
|
**License**: MIT
|
||||||
|
**Language**: TypeScript
|
||||||
|
**Runtime**: Node.js 20
|
||||||
|
**Framework**: Fastify with opinionated architecture
|
||||||
|
|
||||||
|
## Core Technology Stack
|
||||||
|
|
||||||
|
- **Runtime**: Node.js 20
|
||||||
|
- **Language**: TypeScript (strict mode enabled)
|
||||||
|
- **Web Framework**: Fastify 5
|
||||||
|
- **Database**: PostgreSQL with Prisma ORM
|
||||||
|
- **Validation**: Zod
|
||||||
|
- **HTTP Client**: Axios
|
||||||
|
- **Real-time**: Socket.io
|
||||||
|
- **Cache/Pub-Sub**: Redis (via ioredis)
|
||||||
|
- **Testing**: Vitest
|
||||||
|
- **Package Manager**: Yarn (not npm)
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- `yarn build` - TypeScript type checking
|
||||||
|
- `yarn start` - Start the server
|
||||||
|
- `yarn test` - Run tests
|
||||||
|
- `yarn migrate` - Run Prisma migrations
|
||||||
|
- `yarn generate` - Generate Prisma client
|
||||||
|
- `yarn db` - Start local PostgreSQL in Docker
|
||||||
|
|
||||||
|
### Environment Requirements
|
||||||
|
- FFmpeg installed (for media processing)
|
||||||
|
- Python3 installed
|
||||||
|
- PostgreSQL database
|
||||||
|
- Redis (for event bus and caching)
|
||||||
|
|
||||||
|
## Code Style and Structure
|
||||||
|
|
||||||
|
### General Principles
|
||||||
|
- Use 4 spaces for tabs (not 2 spaces)
|
||||||
|
- Write concise, technical TypeScript code with accurate examples
|
||||||
|
- Use functional and declarative programming patterns; avoid classes
|
||||||
|
- Prefer iteration and modularization over code duplication
|
||||||
|
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
|
||||||
|
- All sources must be imported using "@/" prefix (e.g., `import "@/utils/log"`)
|
||||||
|
- Always use absolute imports
|
||||||
|
- Prefer interfaces over types
|
||||||
|
- Avoid enums; use maps instead
|
||||||
|
- Use strict mode in TypeScript for better type safety
|
||||||
|
|
||||||
|
### Folder Structure
|
||||||
|
```
|
||||||
|
/sources # Root of the sources
|
||||||
|
├── /app # Application entry points
|
||||||
|
│ ├── api.ts # API server setup
|
||||||
|
│ └── timeout.ts # Timeout handling
|
||||||
|
├── /apps # Applications directory
|
||||||
|
│ └── /api # API server application
|
||||||
|
│ └── /routes # API routes
|
||||||
|
├── /modules # Reusable modules (non-application logic)
|
||||||
|
├── /utils # Low level or abstract utilities
|
||||||
|
├── /recipes # Scripts to run outside of the server
|
||||||
|
├── /services # Core services
|
||||||
|
│ └── pubsub.ts # Pub/sub service
|
||||||
|
├── /storage # Database and storage utilities
|
||||||
|
│ ├── db.ts # Database client
|
||||||
|
│ ├── inTx.ts # Transaction wrapper
|
||||||
|
│ ├── repeatKey.ts # Key utilities
|
||||||
|
│ ├── simpleCache.ts # Caching utility
|
||||||
|
│ └── types.ts # Storage types
|
||||||
|
└── main.ts # Main entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- Use lowercase with dashes for directories (e.g., components/auth-wizard)
|
||||||
|
- When writing utility functions, always name file and function the same way
|
||||||
|
- Test files should have ".spec.ts" suffix
|
||||||
|
|
||||||
|
## Tool Usage
|
||||||
|
|
||||||
|
### Web Search and Fetching
|
||||||
|
- When in doubt, use web tool to get answers from the web
|
||||||
|
- Search web when you have some failures
|
||||||
|
|
||||||
|
### File Operations
|
||||||
|
- NEVER create files unless they're absolutely necessary
|
||||||
|
- ALWAYS prefer editing existing files to creating new ones
|
||||||
|
- NEVER proactively create documentation files (*.md) or README files unless explicitly requested
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
### Writing Utility Functions
|
||||||
|
1. Always name file and function the same way for easy discovery
|
||||||
|
2. Utility functions should be modular and not too complex
|
||||||
|
3. Always write tests for utility functions BEFORE writing the code
|
||||||
|
4. Iterate implementation and tests until the function works as expected
|
||||||
|
5. Always write documentation for utility functions
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
### Module Guidelines
|
||||||
|
- Modules are bigger than utility functions and abstract away complexity
|
||||||
|
- Each module should have a dedicated directory
|
||||||
|
- Modules usually don't have application-specific logic
|
||||||
|
- Modules can depend on other modules, but not on application-specific logic
|
||||||
|
- Prefer to write code as modules instead of application-specific code
|
||||||
|
|
||||||
|
### When to Use Modules
|
||||||
|
- When integrating with external services
|
||||||
|
- When abstracting complexity of some library
|
||||||
|
- When implementing related groups of functions (math, date, etc.)
|
||||||
|
|
||||||
|
### Known Modules
|
||||||
|
- **ai**: AI wrappers to interact with AI services
|
||||||
|
- **eventbus**: Event bus to send and receive events between modules and applications
|
||||||
|
- **lock**: Simple lock to synchronize access to resources in the whole cluster
|
||||||
|
- **media**: Tools to work with media files
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
- Applications contain application-specific logic
|
||||||
|
- Applications have the most complexity; other parts should assist by reducing complexity
|
||||||
|
- When using prompts, write them to "_prompts.ts" file relative to the application
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
### Prisma Usage
|
||||||
|
- Prisma is used as ORM
|
||||||
|
- Use "inTx" to wrap database operations in transactions
|
||||||
|
- Do not update schema without absolute necessity
|
||||||
|
- For complex fields, use "Json" type
|
||||||
|
|
||||||
|
### Current Schema Status
|
||||||
|
The project has pending Prisma migrations that need to be applied:
|
||||||
|
- Migration: `20250715012822_add_metadata_version_agent_state`
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
### Event Bus
|
||||||
|
- eventbus allows sending and receiving events inside the process and between different processes
|
||||||
|
- eventbus is local or redis based
|
||||||
|
- Use "afterTx" to send events after transaction is committed successfully instead of directly emitting events
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Write tests using Vitest
|
||||||
|
- Test files should be named the same as source files with ".spec.ts" suffix
|
||||||
|
- For utility functions, write tests BEFORE implementation
|
||||||
|
|
||||||
|
## API Development
|
||||||
|
|
||||||
|
- API server is in `/sources/apps/api`
|
||||||
|
- Routes are in `/sources/apps/api/routes`
|
||||||
|
- Use Fastify with Zod for type-safe route definitions
|
||||||
|
- Always validate inputs using Zod
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
The project includes a multi-stage Dockerfile:
|
||||||
|
1. Builder stage: Installs dependencies and builds the application
|
||||||
|
2. Runner stage: Minimal runtime with only necessary files
|
||||||
|
3. Exposes port 3000
|
||||||
|
4. Requires FFmpeg and Python3 in the runtime
|
||||||
|
|
||||||
|
## Important Reminders
|
||||||
|
|
||||||
|
1. Do what has been asked; nothing more, nothing less
|
||||||
|
2. NEVER create files unless they're absolutely necessary for achieving your goal
|
||||||
|
3. ALWAYS prefer editing an existing file to creating a new one
|
||||||
|
4. NEVER proactively create documentation files (*.md) or README files unless explicitly requested
|
||||||
|
5. Use 4 spaces for tabs (not 2 spaces)
|
||||||
|
6. Use yarn instead of npm for all package management
|
@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" ADD COLUMN "agentState" TEXT,
|
||||||
|
ADD COLUMN "agentStateVersion" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN "metadataVersion" INTEGER NOT NULL DEFAULT 0;
|
@ -33,17 +33,20 @@ model Account {
|
|||||||
//
|
//
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tag String
|
tag String
|
||||||
accountId String
|
accountId String
|
||||||
account Account @relation(fields: [accountId], references: [id])
|
account Account @relation(fields: [accountId], references: [id])
|
||||||
metadata String
|
metadata String
|
||||||
seq Int @default(0)
|
metadataVersion Int @default(0)
|
||||||
active Boolean @default(true)
|
agentState String?
|
||||||
lastActiveAt DateTime @default(now())
|
agentStateVersion Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
seq Int @default(0)
|
||||||
updatedAt DateTime @updatedAt
|
active Boolean @default(true)
|
||||||
messages SessionMessage[]
|
lastActiveAt DateTime @default(now())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
messages SessionMessage[]
|
||||||
|
|
||||||
@@unique([accountId, tag])
|
@@unique([accountId, tag])
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,9 @@ export async function startApi() {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
metadataVersion: true,
|
||||||
|
agentState: true,
|
||||||
|
agentStateVersion: true,
|
||||||
active: true,
|
active: true,
|
||||||
lastActiveAt: true,
|
lastActiveAt: true,
|
||||||
messages: {
|
messages: {
|
||||||
@ -152,6 +155,9 @@ export async function startApi() {
|
|||||||
active: v.active,
|
active: v.active,
|
||||||
activeAt: v.lastActiveAt.getTime(),
|
activeAt: v.lastActiveAt.getTime(),
|
||||||
metadata: v.metadata,
|
metadata: v.metadata,
|
||||||
|
metadataVersion: v.metadataVersion,
|
||||||
|
agentState: v.agentState,
|
||||||
|
agentStateVersion: v.agentStateVersion,
|
||||||
lastMessage: v.messages[0] ? {
|
lastMessage: v.messages[0] ? {
|
||||||
id: v.messages[0].id,
|
id: v.messages[0].id,
|
||||||
seq: v.messages[0].seq,
|
seq: v.messages[0].seq,
|
||||||
@ -167,7 +173,8 @@ export async function startApi() {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
metadata: z.string()
|
metadata: z.string(),
|
||||||
|
agentState: z.string().nullish()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
preHandler: app.authenticate
|
preHandler: app.authenticate
|
||||||
@ -187,6 +194,9 @@ export async function startApi() {
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
seq: session.seq,
|
seq: session.seq,
|
||||||
metadata: session.metadata,
|
metadata: session.metadata,
|
||||||
|
metadataVersion: session.metadataVersion,
|
||||||
|
agentState: session.agentState,
|
||||||
|
agentStateVersion: session.agentStateVersion,
|
||||||
active: session.active,
|
active: session.active,
|
||||||
activeAt: session.lastActiveAt.getTime(),
|
activeAt: session.lastActiveAt.getTime(),
|
||||||
createdAt: session.createdAt.getTime(),
|
createdAt: session.createdAt.getTime(),
|
||||||
@ -221,7 +231,10 @@ export async function startApi() {
|
|||||||
t: 'new-session',
|
t: 'new-session',
|
||||||
id: session.id,
|
id: session.id,
|
||||||
seq: session.seq,
|
seq: session.seq,
|
||||||
metadata: metadata,
|
metadata: session.metadata,
|
||||||
|
metadataVersion: session.metadataVersion,
|
||||||
|
agentState: session.agentState,
|
||||||
|
agentStateVersion: session.agentStateVersion,
|
||||||
active: session.active,
|
active: session.active,
|
||||||
activeAt: session.lastActiveAt.getTime(),
|
activeAt: session.lastActiveAt.getTime(),
|
||||||
createdAt: session.createdAt.getTime(),
|
createdAt: session.createdAt.getTime(),
|
||||||
@ -253,6 +266,9 @@ export async function startApi() {
|
|||||||
id: result.session.id,
|
id: result.session.id,
|
||||||
seq: result.session.seq,
|
seq: result.session.seq,
|
||||||
metadata: result.session.metadata,
|
metadata: result.session.metadata,
|
||||||
|
metadataVersion: result.session.metadataVersion,
|
||||||
|
agentState: result.session.agentState,
|
||||||
|
agentStateVersion: result.session.agentStateVersion,
|
||||||
active: result.session.active,
|
active: result.session.active,
|
||||||
activeAt: result.session.lastActiveAt.getTime(),
|
activeAt: result.session.lastActiveAt.getTime(),
|
||||||
createdAt: result.session.createdAt.getTime(),
|
createdAt: result.session.createdAt.getTime(),
|
||||||
@ -575,6 +591,91 @@ export async function startApi() {
|
|||||||
pubsub.emit('update', userId, result.update);
|
pubsub.emit('update', userId, result.update);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('update-metadata', async (data: any, callback: (response: any) => void) => {
|
||||||
|
const { sid, metadata, expectedVersion } = data;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!sid || typeof metadata !== 'string' || typeof expectedVersion !== 'number') {
|
||||||
|
if (callback) {
|
||||||
|
callback({ result: 'error' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction to ensure consistency
|
||||||
|
const result = await db.$transaction(async (tx) => {
|
||||||
|
// Verify session belongs to user and lock it
|
||||||
|
const session = await tx.session.findFirst({
|
||||||
|
where: {
|
||||||
|
id: sid,
|
||||||
|
accountId: userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const user = await tx.account.findUnique({
|
||||||
|
where: { id: userId }
|
||||||
|
});
|
||||||
|
if (!user || !session) {
|
||||||
|
callback({ result: 'error' });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version
|
||||||
|
if (session.metadataVersion !== expectedVersion) {
|
||||||
|
callback({ result: 'version-mismatch', version: session.metadataVersion, metadata: session.metadata });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next sequence number
|
||||||
|
const updSeq = user.seq + 1;
|
||||||
|
const newMetadataVersion = session.metadataVersion + 1;
|
||||||
|
|
||||||
|
// Update session metadata
|
||||||
|
await tx.session.update({
|
||||||
|
where: { id: sid },
|
||||||
|
data: {
|
||||||
|
metadata: metadata,
|
||||||
|
metadataVersion: newMetadataVersion
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create update
|
||||||
|
const updContent: PrismaJson.UpdateBody = {
|
||||||
|
t: 'update-session',
|
||||||
|
id: sid,
|
||||||
|
metadata: {
|
||||||
|
value: metadata,
|
||||||
|
version: newMetadataVersion
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = await tx.update.create({
|
||||||
|
data: {
|
||||||
|
accountId: userId,
|
||||||
|
seq: updSeq,
|
||||||
|
content: updContent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update user sequence
|
||||||
|
await tx.account.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { seq: updSeq }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { update, newMetadataVersion };
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit update to connected sockets
|
||||||
|
pubsub.emit('update', userId, result.update);
|
||||||
|
|
||||||
|
// Send success response with new version via callback
|
||||||
|
callback({ success: true, metadataVersion: result.newMetadataVersion, metadata: metadata });
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
socket.emit('auth', { success: true, user: userId });
|
socket.emit('auth', { success: true, user: userId });
|
||||||
log({ module: 'websocket' }, `User connected: ${userId}`);
|
log({ module: 'websocket' }, `User connected: ${userId}`);
|
||||||
});
|
});
|
||||||
|
@ -22,10 +22,24 @@ declare global {
|
|||||||
id: string;
|
id: string;
|
||||||
seq: number;
|
seq: number;
|
||||||
metadata: string;
|
metadata: string;
|
||||||
|
metadataVersion: number;
|
||||||
|
agentState: string | null;
|
||||||
|
agentStateVersion: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
activeAt: number;
|
activeAt: number;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
} | {
|
||||||
|
t: 'update-session'
|
||||||
|
id: string;
|
||||||
|
metadata?: {
|
||||||
|
value: string;
|
||||||
|
version: number;
|
||||||
|
} | null | undefined
|
||||||
|
agentState?: {
|
||||||
|
value: string;
|
||||||
|
version: number;
|
||||||
|
} | null | undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user