Compare commits
3 Commits
140e457a72
...
21c045559d
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c045559d | |||
| 719f661963 | |||
| 1035d13fc0 |
@@ -23,7 +23,7 @@ import { createFileTools } from './tools/file-tools.js';
|
||||
import { createGitTools } from './tools/git-tools.js';
|
||||
import { createShellTools } from './tools/shell-tools.js';
|
||||
import { createWebTools } from './tools/web-tools.js';
|
||||
import type { SessionInfoDto } from './session.dto.js';
|
||||
import type { SessionInfoDto, SessionMetrics } from './session.dto.js';
|
||||
import { SystemOverrideService } from '../preferences/system-override.service.js';
|
||||
import { PreferencesService } from '../preferences/preferences.service.js';
|
||||
import { SessionGCService } from '../gc/session-gc.service.js';
|
||||
@@ -97,6 +97,8 @@ export interface AgentSession {
|
||||
agentConfigId?: string;
|
||||
/** Human-readable agent name applied to this session, if any (M5-001). */
|
||||
agentName?: string;
|
||||
/** M5-007: per-session metrics. */
|
||||
metrics: SessionMetrics;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -340,6 +342,12 @@ export class AgentService implements OnModuleDestroy {
|
||||
userId: mergedOptions?.userId,
|
||||
agentConfigId: mergedOptions?.agentConfigId,
|
||||
agentName: resolvedAgentName,
|
||||
metrics: {
|
||||
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
modelSwitches: 0,
|
||||
messageCount: 0,
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
@@ -467,24 +475,95 @@ export class AgentService implements OnModuleDestroy {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
listSessions(): SessionInfoDto[] {
|
||||
const now = Date.now();
|
||||
return Array.from(this.sessions.values()).map((s) => ({
|
||||
id: s.id,
|
||||
provider: s.provider,
|
||||
modelId: s.modelId,
|
||||
...(s.agentName ? { agentName: s.agentName } : {}),
|
||||
createdAt: new Date(s.createdAt).toISOString(),
|
||||
promptCount: s.promptCount,
|
||||
channels: Array.from(s.channels),
|
||||
durationMs: now - s.createdAt,
|
||||
metrics: { ...s.metrics },
|
||||
}));
|
||||
}
|
||||
|
||||
getSessionInfo(sessionId: string): SessionInfoDto | undefined {
|
||||
const s = this.sessions.get(sessionId);
|
||||
if (!s) return undefined;
|
||||
return {
|
||||
id: s.id,
|
||||
provider: s.provider,
|
||||
modelId: s.modelId,
|
||||
...(s.agentName ? { agentName: s.agentName } : {}),
|
||||
createdAt: new Date(s.createdAt).toISOString(),
|
||||
promptCount: s.promptCount,
|
||||
channels: Array.from(s.channels),
|
||||
durationMs: Date.now() - s.createdAt,
|
||||
metrics: { ...s.metrics },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record token usage for a session turn (M5-007).
|
||||
* Accumulates tokens across the session lifetime.
|
||||
*/
|
||||
recordTokenUsage(
|
||||
sessionId: string,
|
||||
tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number },
|
||||
): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
session.metrics.tokens.input += tokens.input;
|
||||
session.metrics.tokens.output += tokens.output;
|
||||
session.metrics.tokens.cacheRead += tokens.cacheRead;
|
||||
session.metrics.tokens.cacheWrite += tokens.cacheWrite;
|
||||
session.metrics.tokens.total += tokens.total;
|
||||
session.metrics.lastActivityAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a model switch event for a session (M5-007).
|
||||
*/
|
||||
recordModelSwitch(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
session.metrics.modelSwitches += 1;
|
||||
session.metrics.lastActivityAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment message count for a session (M5-007).
|
||||
*/
|
||||
recordMessage(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
session.metrics.messageCount += 1;
|
||||
session.metrics.lastActivityAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the model tracked on a live session (M5-002).
|
||||
* Records the model change in session metadata so subsequent session:info
|
||||
* emissions reflect the new model. The Pi session itself is not reconstructed —
|
||||
* the new model takes effect on the next message prompt.
|
||||
* This records the model change in the session metadata so subsequent
|
||||
* session:info emissions reflect the new model. The Pi session itself is
|
||||
* not reconstructed — the model is used on the next createSession call for
|
||||
* the same conversationId when the session is torn down or a new one is created.
|
||||
*/
|
||||
updateSessionModel(sessionId: string, modelId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
const prev = session.modelId;
|
||||
session.modelId = modelId;
|
||||
this.recordModelSwitch(sessionId);
|
||||
this.logger.log(`Session ${sessionId}: model updated ${prev} → ${modelId} (M5-002)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a new agent config to a live session mid-conversation (M5-003).
|
||||
* Updates agentName, agentConfigId, and modelId on the session object.
|
||||
* System prompt and tools take effect when a new session is created for
|
||||
* System prompt and tools take effect when the next session is created for
|
||||
* this conversationId (they are baked in at session creation time).
|
||||
*/
|
||||
applyAgentConfig(
|
||||
@@ -505,35 +584,6 @@ export class AgentService implements OnModuleDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
listSessions(): SessionInfoDto[] {
|
||||
const now = Date.now();
|
||||
return Array.from(this.sessions.values()).map((s) => ({
|
||||
id: s.id,
|
||||
provider: s.provider,
|
||||
modelId: s.modelId,
|
||||
...(s.agentName ? { agentName: s.agentName } : {}),
|
||||
createdAt: new Date(s.createdAt).toISOString(),
|
||||
promptCount: s.promptCount,
|
||||
channels: Array.from(s.channels),
|
||||
durationMs: now - s.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
getSessionInfo(sessionId: string): SessionInfoDto | undefined {
|
||||
const s = this.sessions.get(sessionId);
|
||||
if (!s) return undefined;
|
||||
return {
|
||||
id: s.id,
|
||||
provider: s.provider,
|
||||
modelId: s.modelId,
|
||||
...(s.agentName ? { agentName: s.agentName } : {}),
|
||||
createdAt: new Date(s.createdAt).toISOString(),
|
||||
promptCount: s.promptCount,
|
||||
channels: Array.from(s.channels),
|
||||
durationMs: Date.now() - s.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
addChannel(sessionId: string, channel: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
/** Token usage metrics for a session (M5-007). */
|
||||
export interface SessionTokenMetrics {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** Per-session metrics tracked throughout the session lifetime (M5-007). */
|
||||
export interface SessionMetrics {
|
||||
tokens: SessionTokenMetrics;
|
||||
modelSwitches: number;
|
||||
messageCount: number;
|
||||
lastActivityAt: string;
|
||||
}
|
||||
|
||||
export interface SessionInfoDto {
|
||||
id: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
/** Human-readable agent name when an agent config is applied (M5-001). */
|
||||
/** M5-005: human-readable agent name when an agent config is applied. */
|
||||
agentName?: string;
|
||||
createdAt: string;
|
||||
promptCount: number;
|
||||
channels: string[];
|
||||
durationMs: number;
|
||||
/** M5-007: per-session metrics (token usage, model switches, etc.) */
|
||||
metrics: SessionMetrics;
|
||||
}
|
||||
|
||||
export interface SessionListDto {
|
||||
|
||||
@@ -119,6 +119,17 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
// When resuming an existing conversation, load prior messages to inject as context (M1-004)
|
||||
const conversationHistory = await this.loadConversationHistory(conversationId, userId);
|
||||
|
||||
// M5-004: Check if there's an existing sessionId bound to this conversation
|
||||
let existingSessionId: string | undefined;
|
||||
if (userId) {
|
||||
existingSessionId = await this.getConversationSessionId(conversationId, userId);
|
||||
if (existingSessionId) {
|
||||
this.logger.log(
|
||||
`Resuming existing sessionId=${existingSessionId} for conversation=${conversationId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine provider/model via routing engine or per-session /model override (M4-012 / M4-007)
|
||||
let resolvedProvider = data.provider;
|
||||
let resolvedModelId = data.modelId;
|
||||
@@ -153,7 +164,9 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
}
|
||||
}
|
||||
|
||||
agentSession = await this.agentService.createSession(conversationId, {
|
||||
// M5-004: Use existingSessionId as sessionId when available (session reuse)
|
||||
const sessionIdToCreate = existingSessionId ?? conversationId;
|
||||
agentSession = await this.agentService.createSession(sessionIdToCreate, {
|
||||
provider: resolvedProvider,
|
||||
modelId: resolvedModelId,
|
||||
agentConfigId: data.agentId,
|
||||
@@ -180,10 +193,15 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
}
|
||||
|
||||
// Ensure conversation record exists in the DB before persisting messages
|
||||
// M5-004: Also bind the sessionId to the conversation record
|
||||
if (userId) {
|
||||
await this.ensureConversation(conversationId, userId);
|
||||
await this.bindSessionToConversation(conversationId, userId, conversationId);
|
||||
}
|
||||
|
||||
// M5-007: Count the user message
|
||||
this.agentService.recordMessage(conversationId);
|
||||
|
||||
// Persist the user message
|
||||
if (userId) {
|
||||
try {
|
||||
@@ -234,6 +252,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
this.agentService.addChannel(conversationId, `websocket:${client.id}`);
|
||||
|
||||
// Send session info so the client knows the model/provider (M4-008: include routing decision)
|
||||
// Include agentName when a named agent config is active (M5-001)
|
||||
{
|
||||
const agentSession = this.agentService.getSession(conversationId);
|
||||
if (agentSession) {
|
||||
@@ -325,17 +344,18 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
* Set a per-conversation model override (M4-007 / M5-002).
|
||||
* When set, the routing engine is bypassed and the specified model is used.
|
||||
* Pass null to clear the override and resume automatic routing.
|
||||
* M5-002: Also updates the live session's modelId and emits session:info.
|
||||
* M5-005: Emits session:info to clients subscribed to this conversation when a model is set.
|
||||
* M5-007: Records a model switch in session metrics.
|
||||
*/
|
||||
setModelOverride(conversationId: string, modelName: string | null): void {
|
||||
if (modelName) {
|
||||
modelOverrides.set(conversationId, modelName);
|
||||
this.logger.log(`Model override set: conversation=${conversationId} model="${modelName}"`);
|
||||
|
||||
// M5-002: Update live session model so next session:info reflects the new model
|
||||
// M5-002: Update the live session's modelId so session:info reflects the new model immediately
|
||||
this.agentService.updateSessionModel(conversationId, modelName);
|
||||
|
||||
// Broadcast updated session:info to all clients watching this conversation
|
||||
// M5-005: Broadcast session:info to all clients subscribed to this conversation
|
||||
this.broadcastSessionInfo(conversationId);
|
||||
} else {
|
||||
modelOverrides.delete(conversationId);
|
||||
@@ -351,10 +371,13 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast session:info to all clients currently subscribed to a conversation (M5-002/M5-003).
|
||||
* M5-005: Broadcast session:info to all clients currently subscribed to a conversation.
|
||||
* Called on model or agent switch to ensure the TUI TopBar updates immediately.
|
||||
*/
|
||||
broadcastSessionInfo(conversationId: string, extra?: { agentName?: string }): void {
|
||||
broadcastSessionInfo(
|
||||
conversationId: string,
|
||||
extra?: { agentName?: string; routingDecision?: RoutingDecisionInfo },
|
||||
): void {
|
||||
const agentSession = this.agentService.getSession(conversationId);
|
||||
if (!agentSession) return;
|
||||
|
||||
@@ -367,6 +390,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
thinkingLevel: piSession.thinkingLevel,
|
||||
availableThinkingLevels: piSession.getAvailableThinkingLevels(),
|
||||
...(resolvedAgentName ? { agentName: resolvedAgentName } : {}),
|
||||
...(extra?.routingDecision ? { routingDecision: extra.routingDecision } : {}),
|
||||
};
|
||||
|
||||
// Emit to all clients currently subscribed to this conversation
|
||||
@@ -402,6 +426,45 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* M5-004: Bind the agent sessionId to the conversation record in the DB.
|
||||
* Updates the sessionId column so future resumes can reuse the session.
|
||||
*/
|
||||
private async bindSessionToConversation(
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.brain.conversations.update(conversationId, userId, { sessionId });
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to bind sessionId=${sessionId} to conversation=${conversationId}`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* M5-004: Retrieve the sessionId bound to a conversation, if any.
|
||||
* Returns undefined when the conversation does not exist or has no bound session.
|
||||
*/
|
||||
private async getConversationSessionId(
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const conv = await this.brain.conversations.findById(conversationId, userId);
|
||||
return conv?.sessionId ?? undefined;
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to get sessionId for conversation=${conversationId}`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load prior conversation messages from DB for context injection on session resume (M1-004).
|
||||
* Returns an empty array when no history exists, the conversation is not owned by the user,
|
||||
@@ -478,6 +541,17 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
usage: usagePayload,
|
||||
});
|
||||
|
||||
// M5-007: Accumulate token usage in session metrics
|
||||
if (stats?.tokens) {
|
||||
this.agentService.recordTokenUsage(conversationId, {
|
||||
input: stats.tokens.input ?? 0,
|
||||
output: stats.tokens.output ?? 0,
|
||||
cacheRead: stats.tokens.cacheRead ?? 0,
|
||||
cacheWrite: stats.tokens.cacheWrite ?? 0,
|
||||
total: stats.tokens.total ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Persist the assistant message with metadata
|
||||
const cs = this.clientSessions.get(client.id);
|
||||
const userId = (client.data.user as { id: string } | undefined)?.id;
|
||||
|
||||
@@ -42,7 +42,7 @@ const mockRedis = {
|
||||
|
||||
// Mock agent config returned by brain.agents.findByName for "my-agent-id"
|
||||
const mockAgentConfig = {
|
||||
id: 'agent-uuid-123',
|
||||
id: 'my-agent-id',
|
||||
name: 'my-agent-id',
|
||||
model: 'claude-sonnet-4-6',
|
||||
provider: 'anthropic',
|
||||
@@ -57,11 +57,12 @@ const mockAgentConfig = {
|
||||
|
||||
const mockBrain = {
|
||||
agents: {
|
||||
// findByName resolves with the agent when name matches, undefined otherwise
|
||||
findByName: vi.fn((name: string) =>
|
||||
Promise.resolve(name === 'my-agent-id' ? mockAgentConfig : undefined),
|
||||
),
|
||||
findById: vi.fn((id: string) =>
|
||||
Promise.resolve(id === 'agent-uuid-123' ? mockAgentConfig : undefined),
|
||||
Promise.resolve(id === 'my-agent-id' ? mockAgentConfig : undefined),
|
||||
),
|
||||
create: vi.fn(),
|
||||
},
|
||||
|
||||
@@ -242,13 +242,14 @@ export class CommandExecutorService {
|
||||
private async handleAgent(
|
||||
args: string | null,
|
||||
conversationId: string,
|
||||
_userId: string,
|
||||
userId: string,
|
||||
): Promise<SlashCommandResultPayload> {
|
||||
if (!args) {
|
||||
return {
|
||||
command: 'agent',
|
||||
success: true,
|
||||
message: 'Usage: /agent <agent-name> to switch, or /agent list to see available agents.',
|
||||
message:
|
||||
'Usage: /agent <agent-id> | /agent list | /agent new <name> to create a new agent.',
|
||||
conversationId,
|
||||
};
|
||||
}
|
||||
@@ -262,12 +263,58 @@ export class CommandExecutorService {
|
||||
};
|
||||
}
|
||||
|
||||
// M5-006: /agent new <name> — create a new agent config via brain.agents.create()
|
||||
if (args.startsWith('new')) {
|
||||
const namePart = args.slice(3).trim();
|
||||
if (!namePart) {
|
||||
return {
|
||||
command: 'agent',
|
||||
success: false,
|
||||
message: 'Usage: /agent new <name> — provide a name for the new agent.',
|
||||
conversationId,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const defaultProvider = process.env['DEFAULT_PROVIDER'] ?? 'anthropic';
|
||||
const defaultModel = process.env['DEFAULT_MODEL'] ?? 'claude-sonnet-4-5-20251001';
|
||||
|
||||
const newAgent = await this.brain.agents.create({
|
||||
name: namePart,
|
||||
provider: defaultProvider,
|
||||
model: defaultModel,
|
||||
status: 'idle',
|
||||
ownerId: userId,
|
||||
isSystem: false,
|
||||
});
|
||||
|
||||
this.logger.log(`Created new agent "${newAgent.name}" (${newAgent.id}) for user ${userId}`);
|
||||
|
||||
return {
|
||||
command: 'agent',
|
||||
success: true,
|
||||
message: `Agent "${newAgent.name}" created with ID: ${newAgent.id}. Configure it via the web dashboard.`,
|
||||
conversationId,
|
||||
data: { agentId: newAgent.id, agentName: newAgent.name },
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to create agent: ${err}`);
|
||||
return {
|
||||
command: 'agent',
|
||||
success: false,
|
||||
message: `Failed to create agent: ${String(err)}`,
|
||||
conversationId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// M5-003: Look up agent by name (or ID) and apply to session mid-conversation
|
||||
const agentName = args.trim();
|
||||
try {
|
||||
// Try lookup by name first; fall back to ID lookup
|
||||
// Try lookup by name first; fall back to ID-based lookup
|
||||
let agentConfig = await this.brain.agents.findByName(agentName);
|
||||
if (!agentConfig) {
|
||||
// Try by ID (UUID-style input)
|
||||
agentConfig = await this.brain.agents.findById(agentName);
|
||||
}
|
||||
|
||||
@@ -280,7 +327,7 @@ export class CommandExecutorService {
|
||||
};
|
||||
}
|
||||
|
||||
// Apply agent config to the live session metadata (M5-003)
|
||||
// Apply the agent config to the live session and emit session:info (M5-003)
|
||||
this.agentService.applyAgentConfig(
|
||||
conversationId,
|
||||
agentConfig.id,
|
||||
@@ -288,7 +335,7 @@ export class CommandExecutorService {
|
||||
agentConfig.model ?? undefined,
|
||||
);
|
||||
|
||||
// Broadcast updated session:info so TUI reflects new agent/model (M5-003)
|
||||
// Broadcast updated session:info so TUI TopBar reflects new agent/model
|
||||
this.chatGateway?.broadcastSessionInfo(conversationId, { agentName: agentConfig.name });
|
||||
|
||||
this.logger.log(
|
||||
@@ -298,7 +345,7 @@ export class CommandExecutorService {
|
||||
return {
|
||||
command: 'agent',
|
||||
success: true,
|
||||
message: `Switched to agent "${agentConfig.name}". Model: ${agentConfig.model ?? 'default'}.`,
|
||||
message: `Switched to agent "${agentConfig.name}". System prompt and tools applied. Model: ${agentConfig.model ?? 'default'}.`,
|
||||
conversationId,
|
||||
data: { agentId: agentConfig.id, agentName: agentConfig.name, model: agentConfig.model },
|
||||
};
|
||||
|
||||
@@ -39,14 +39,6 @@ const mockRedis = {
|
||||
keys: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function buildRegistry(): CommandRegistryService {
|
||||
const svc = new CommandRegistryService();
|
||||
svc.onModuleInit(); // seed core commands
|
||||
return svc;
|
||||
}
|
||||
|
||||
const mockBrain = {
|
||||
agents: {
|
||||
findByName: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -55,6 +47,14 @@ const mockBrain = {
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function buildRegistry(): CommandRegistryService {
|
||||
const svc = new CommandRegistryService();
|
||||
svc.onModuleInit(); // seed core commands
|
||||
return svc;
|
||||
}
|
||||
|
||||
function buildExecutor(registry: CommandRegistryService): CommandExecutorService {
|
||||
return new CommandExecutorService(
|
||||
registry as never,
|
||||
|
||||
1
packages/db/drizzle/0006_swift_shen.sql
Normal file
1
packages/db/drizzle/0006_swift_shen.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "conversations" ADD COLUMN "session_id" text;
|
||||
2768
packages/db/drizzle/meta/0006_snapshot.json
Normal file
2768
packages/db/drizzle/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,13 @@
|
||||
"when": 1774225763410,
|
||||
"tag": "0005_minor_champions",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1774227064500,
|
||||
"tag": "0006_swift_shen",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -319,6 +319,8 @@ export const conversations = pgTable(
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
||||
agentId: uuid('agent_id').references(() => agents.id, { onDelete: 'set null' }),
|
||||
/** M5-004: Agent session ID bound to this conversation. Nullable — set when a session is created. */
|
||||
sessionId: text('session_id'),
|
||||
archived: boolean('archived').notNull().default(false),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
Reference in New Issue
Block a user