Skip to content

Commit 74df2ab

Browse files
committed
fix(chat): log only turns this call finalized, include recovered turns, and resolve the email at send time
1 parent a1c00a2 commit 74df2ab

5 files changed

Lines changed: 63 additions & 37 deletions

File tree

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1+
import { db } from '@sim/db'
2+
import { user } from '@sim/db/schema'
13
import { createLogger } from '@sim/logger'
24
import { getErrorMessage } from '@sim/utils/errors'
5+
import { eq } from 'drizzle-orm'
36
import { env } from '@/lib/core/config/env'
47
import type { OrchestratorResult } from '@/lib/mothership/request/types'
58

69
const logger = createLogger('ChatLog')
710

811
const CHAT_LOG_TIMEOUT_MS = 5_000
912

10-
/** The finished turn's identity, captured when the user's message is admitted. */
13+
/** The turn a Chat send admitted — rebuilt identically when a relay pod recovers it. */
1114
export interface ChatTurnLogContext {
1215
chatId: string
1316
messageId: string
1417
requestId: string
1518
userId: string
16-
userEmail?: string
1719
userMessage: string
1820
mode: 'assistant' | 'agent' | 'plan'
1921
startedAt: number
@@ -34,6 +36,28 @@ export function logChatTurn(
3436
): void {
3537
const url = env.SIM_LOGGING_WORKFLOW_URL
3638
if (!url) return
39+
const durationMs = Date.now() - context.startedAt
40+
void sendChatTurn(url, context, result, status, durationMs).catch((error) => {
41+
logger.warn('Chat log workflow request failed', {
42+
chatId: context.chatId,
43+
requestId: context.requestId,
44+
error: getErrorMessage(error),
45+
})
46+
})
47+
}
48+
49+
async function sendChatTurn(
50+
url: string,
51+
context: ChatTurnLogContext,
52+
result: OrchestratorResult,
53+
status: ChatTurnStatus,
54+
durationMs: number
55+
): Promise<void> {
56+
const [row] = await db
57+
.select({ email: user.email })
58+
.from(user)
59+
.where(eq(user.id, context.userId))
60+
.limit(1)
3761

3862
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
3963
if (env.SIM_LOGGING_WORKFLOW_API_KEY) headers['X-API-Key'] = env.SIM_LOGGING_WORKFLOW_API_KEY
@@ -45,7 +69,7 @@ export function logChatTurn(
4569
chatId: context.chatId,
4670
messageId: context.messageId,
4771
userId: context.userId,
48-
userEmail: context.userEmail,
72+
userEmail: row?.email,
4973
userMessage: context.userMessage,
5074
assistantResponse: result.content,
5175
status,
@@ -55,34 +79,25 @@ export function logChatTurn(
5579
mode: context.mode,
5680
source: 'workspace-chat',
5781
startedAt: new Date(context.startedAt).toISOString(),
58-
durationMs: Date.now() - context.startedAt,
82+
durationMs,
5983
usage: {
6084
inputTokens: result.usage?.prompt ?? 0,
6185
outputTokens: result.usage?.completion ?? 0,
6286
},
6387
}
6488

65-
void fetch(url, {
89+
const response = await fetch(url, {
6690
method: 'POST',
6791
headers,
6892
body: JSON.stringify({ input }),
6993
signal: AbortSignal.timeout(CHAT_LOG_TIMEOUT_MS),
7094
})
71-
.then(async (response) => {
72-
await response.body?.cancel()
73-
if (!response.ok) {
74-
logger.warn('Chat log workflow returned a non-2xx status', {
75-
status: response.status,
76-
chatId: context.chatId,
77-
requestId: context.requestId,
78-
})
79-
}
80-
})
81-
.catch((error) => {
82-
logger.warn('Chat log workflow request failed', {
83-
chatId: context.chatId,
84-
requestId: context.requestId,
85-
error: getErrorMessage(error),
86-
})
95+
await response.body?.cancel()
96+
if (!response.ok) {
97+
logger.warn('Chat log workflow returned a non-2xx status', {
98+
status: response.status,
99+
chatId: context.chatId,
100+
requestId: context.requestId,
87101
})
102+
}
88103
}

‎apps/sim/lib/mothership/chat/completion.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function buildOnComplete(params: {
2424
organizationId?: string
2525
userId?: string
2626
requestMode?: 'assistant' | 'agent' | 'plan'
27-
/** Present for Chat turns that feed the operator chat log. */
27+
/** Present for workspace and organization Chat turns, which feed the operator chat log. */
2828
chatLog?: ChatTurnLogContext
2929
/**
3030
* Root agent span for this request. When present, the final
@@ -102,7 +102,7 @@ export function buildOnComplete(params: {
102102
const assistantMessage = buildPersistedAssistantMessage(result, requestId, params.requestMode)
103103
const hasPartial =
104104
!!assistantMessage.content?.trim() || (assistantMessage.contentBlocks?.length ?? 0) > 0
105-
await finalizeAssistantTurn({
105+
const finalization = await finalizeAssistantTurn({
106106
runController,
107107
chatId,
108108
userMessageId,
@@ -112,7 +112,9 @@ export function buildOnComplete(params: {
112112
...(result.success ? {} : { streamMarkerPolicy: 'active-or-cleared' as const }),
113113
})
114114

115-
if (chatLog) logChatTurn(chatLog, result, result.success ? 'success' : 'error')
115+
if (chatLog && finalization.updated) {
116+
logChatTurn(chatLog, result, result.success ? 'success' : 'error')
117+
}
116118
if (notifyChatStatus) {
117119
publishChatStatusChanged(
118120
{ workspaceId, organizationId, userId },
@@ -144,7 +146,7 @@ export function buildOnError(params: {
144146
organizationId?: string
145147
userId?: string
146148
requestMode?: 'assistant' | 'agent' | 'plan'
147-
/** Present for Chat turns that feed the operator chat log. */
149+
/** Present for workspace and organization Chat turns, which feed the operator chat log. */
148150
chatLog?: ChatTurnLogContext
149151
}) {
150152
const {
@@ -176,15 +178,15 @@ export function buildOnError(params: {
176178
error: result?.error || getErrorMessage(error),
177179
}
178180
const assistantMessage = buildPersistedAssistantMessage(failed, requestId, params.requestMode)
179-
await finalizeAssistantTurn({
181+
const finalization = await finalizeAssistantTurn({
180182
runController,
181183
chatId,
182184
userMessageId,
183185
assistantMessage,
184186
streamMarkerPolicy: 'active-or-cleared',
185187
})
186188

187-
if (chatLog) logChatTurn(chatLog, failed, 'error')
189+
if (chatLog && finalization.updated) logChatTurn(chatLog, failed, 'error')
188190
if (notifyChatStatus) {
189191
publishChatStatusChanged(
190192
{ workspaceId, organizationId, userId },

‎apps/sim/lib/mothership/chat/post.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,6 @@ export async function handleUnifiedChatPost(req: NextRequest) {
986986
let requestId = ''
987987
const executionId = generateId()
988988
const runId = generateId()
989-
const startedAt = Date.now()
990989

991990
try {
992991
const session = await getSession()
@@ -1489,18 +1488,16 @@ export async function handleUnifiedChatPost(req: NextRequest) {
14891488
}
14901489
const requestMode =
14911490
body.mode === 'plan' ? 'plan' : body.mode === 'assistant' ? 'assistant' : 'agent'
1492-
/** Workspace and organization Chat feed the operator chat log; the workflow panel does not. */
14931491
const chatLog: ChatTurnLogContext | undefined =
14941492
branch.kind !== 'workflow' && actualChatId
14951493
? {
14961494
chatId: actualChatId,
14971495
messageId: userMessageId,
14981496
requestId,
14991497
userId: authenticatedUserId,
1500-
...(authenticatedUserEmail ? { userEmail: authenticatedUserEmail } : {}),
15011498
userMessage: body.message,
15021499
mode: requestMode,
1503-
startedAt,
1500+
startedAt: Date.now(),
15041501
}
15051502
: undefined
15061503
const stream = createSSEStream({

‎apps/sim/lib/mothership/request/application/recover-stream.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const run = {
7373
workspaceId: '33333333-3333-4333-8333-333333333333',
7474
status: 'active',
7575
workflowId: null,
76+
startedAt: new Date('2026-01-01T00:00:00Z'),
7677
requestContext: {
7778
requestId: 'request',
7879
controllerToken: 'old-controller',

‎apps/sim/lib/mothership/request/application/recover-stream.ts‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
1313
import { getLatestRunForStream } from '@/lib/mothership/async-runs/repository'
1414
import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case'
1515
import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context'
16+
import type { ChatTurnLogContext } from '@/lib/mothership/chat/chat-log'
1617
import { buildOnComplete, buildOnError } from '@/lib/mothership/chat/completion'
1718
import { restoreBillingAdmission } from '@/lib/mothership/request/lifecycle/admission'
1819
import { claimRunController } from '@/lib/mothership/request/lifecycle/controller-ownership'
@@ -121,21 +122,31 @@ export const readChatStream = defineAuthorizedChatUseCase({
121122
if (workspaceId && !userPermission)
122123
throw new OrchestrationError('forbidden', 'Workspace access revoked')
123124
const requestId = typeof saved?.requestId === 'string' ? saved.requestId : generateId()
125+
const requestMode: ChatTurnLogContext['mode'] =
126+
intent.mode === 'plan' ? 'plan' : intent.mode === 'assistant' ? 'assistant' : 'agent'
124127
const completion = {
125128
chatId,
126129
userMessageId: run.streamId,
127130
requestId,
128131
workspaceId,
129132
organizationId,
130133
userId,
131-
requestMode:
132-
intent.mode === 'plan'
133-
? ('plan' as const)
134-
: intent.mode === 'assistant'
135-
? ('assistant' as const)
136-
: ('agent' as const),
134+
requestMode,
137135
notifyWorkspaceStatus: true,
138136
runController: { id: run.id, token: lease.value },
137+
...(config.data.goRoute === '/api/mothership'
138+
? {
139+
chatLog: {
140+
chatId,
141+
messageId: run.streamId,
142+
requestId,
143+
userId,
144+
userMessage: intent.message,
145+
mode: requestMode,
146+
startedAt: run.startedAt.getTime(),
147+
},
148+
}
149+
: {}),
139150
}
140151
const stream = createSSEStream({
141152
userId,

0 commit comments

Comments
 (0)