feat(M5-004,M5-005,M5-006,M5-007): session-conversation binding, session:info broadcast, agent creation from TUI, and session metrics (#321)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #321.
This commit is contained in:
@@ -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) {
|
||||
@@ -244,6 +263,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
modelId: agentSession.modelId,
|
||||
thinkingLevel: piSession.thinkingLevel,
|
||||
availableThinkingLevels: piSession.getAvailableThinkingLevels(),
|
||||
...(agentSession.agentName ? { agentName: agentSession.agentName } : {}),
|
||||
...(routingDecisionToStore ? { routingDecision: routingDecisionToStore } : {}),
|
||||
});
|
||||
}
|
||||
@@ -301,6 +321,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
modelId: session.modelId,
|
||||
thinkingLevel: session.piSession.thinkingLevel,
|
||||
availableThinkingLevels: session.piSession.getAvailableThinkingLevels(),
|
||||
...(session.agentName ? { agentName: session.agentName } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -323,11 +344,23 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
* Set a per-conversation model override (M4-007).
|
||||
* When set, the routing engine is bypassed and the specified model is used.
|
||||
* Pass null to clear the override and resume automatic routing.
|
||||
* 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 the live session's modelId so session:info reflects the new model immediately
|
||||
this.agentService.updateSessionModel(conversationId, modelName);
|
||||
|
||||
// M5-005: Broadcast session:info to all clients subscribed to this conversation
|
||||
const agentSession = this.agentService.getSession(conversationId);
|
||||
if (agentSession) {
|
||||
// Find all clients subscribed to this conversation and emit updated session:info
|
||||
this.broadcastSessionInfo(conversationId);
|
||||
}
|
||||
} else {
|
||||
modelOverrides.delete(conversationId);
|
||||
this.logger.log(`Model override cleared: conversation=${conversationId}`);
|
||||
@@ -341,6 +374,39 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
return modelOverrides.get(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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; routingDecision?: RoutingDecisionInfo },
|
||||
): void {
|
||||
const agentSession = this.agentService.getSession(conversationId);
|
||||
if (!agentSession) return;
|
||||
|
||||
const piSession = agentSession.piSession;
|
||||
const payload = {
|
||||
conversationId,
|
||||
provider: agentSession.provider,
|
||||
modelId: agentSession.modelId,
|
||||
thinkingLevel: piSession.thinkingLevel,
|
||||
availableThinkingLevels: piSession.getAvailableThinkingLevels(),
|
||||
...(extra?.agentName ? { agentName: extra.agentName } : {}),
|
||||
...(extra?.routingDecision ? { routingDecision: extra.routingDecision } : {}),
|
||||
};
|
||||
|
||||
// Emit to all clients currently subscribed to this conversation
|
||||
for (const [clientId, session] of this.clientSessions) {
|
||||
if (session.conversationId === conversationId) {
|
||||
const socket = this.server.sockets.sockets.get(clientId);
|
||||
if (socket?.connected) {
|
||||
socket.emit('session:info', payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a conversation record exists in the DB.
|
||||
* Creates it if absent — safe to call concurrently since a duplicate insert
|
||||
@@ -363,6 +429,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,
|
||||
@@ -439,6 +544,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;
|
||||
|
||||
Reference in New Issue
Block a user