Skip to content

Commit a1c00a2

Browse files
committed
fix(chat): restore the operator chat log after the TypeScript worker cutover
1 parent a0c93d6 commit a1c00a2

4 files changed

Lines changed: 140 additions & 18 deletions

File tree

‎apps/sim/lib/core/config/env.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ export const env = createEnv({
312312

313313
// Monitoring & Analytics
314314
TELEMETRY_ENDPOINT: z.string().url().optional(), // Custom telemetry/analytics endpoint
315+
SIM_LOGGING_WORKFLOW_URL: z.string().url().optional(), // Sim workflow execute URL that receives each finished Chat turn (operator chat log; off when unset)
316+
SIM_LOGGING_WORKFLOW_API_KEY: z.string().min(1).optional(), // X-API-Key for SIM_LOGGING_WORKFLOW_URL
315317
COST_MULTIPLIER: z.number().optional(), // Multiplier for cost calculations
316318
LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(), // Minimum log level to display (defaults to ERROR in production, DEBUG in development)
317319
GRAFANA_OTLP_ENDPOINT: z.string().url().optional(), // Grafana Cloud OTLP HTTP gateway base URL (e.g., https://otlp-gateway-prod-us-east-0.grafana.net/otlp). Trigger.dev exporters append /v1/traces, /v1/logs, /v1/metrics.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { env } from '@/lib/core/config/env'
4+
import type { OrchestratorResult } from '@/lib/mothership/request/types'
5+
6+
const logger = createLogger('ChatLog')
7+
8+
const CHAT_LOG_TIMEOUT_MS = 5_000
9+
10+
/** The finished turn's identity, captured when the user's message is admitted. */
11+
export interface ChatTurnLogContext {
12+
chatId: string
13+
messageId: string
14+
requestId: string
15+
userId: string
16+
userEmail?: string
17+
userMessage: string
18+
mode: 'assistant' | 'agent' | 'plan'
19+
startedAt: number
20+
}
21+
22+
export type ChatTurnStatus = 'success' | 'error' | 'aborted'
23+
24+
/**
25+
* Operator funnel: posts each finished Chat turn to the Sim workflow at
26+
* `SIM_LOGGING_WORKFLOW_URL` (off when unset). The body keeps the shape the Go
27+
* copilot sent — `{ input: { event: 'copilot_request_completed', ... } }` — so the
28+
* existing workflow reads it unchanged. Fire-and-forget: never awaited, never throws.
29+
*/
30+
export function logChatTurn(
31+
context: ChatTurnLogContext,
32+
result: OrchestratorResult,
33+
status: ChatTurnStatus
34+
): void {
35+
const url = env.SIM_LOGGING_WORKFLOW_URL
36+
if (!url) return
37+
38+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
39+
if (env.SIM_LOGGING_WORKFLOW_API_KEY) headers['X-API-Key'] = env.SIM_LOGGING_WORKFLOW_API_KEY
40+
41+
const input = {
42+
event: 'copilot_request_completed',
43+
idempotencyKey: context.messageId,
44+
requestId: context.requestId,
45+
chatId: context.chatId,
46+
messageId: context.messageId,
47+
userId: context.userId,
48+
userEmail: context.userEmail,
49+
userMessage: context.userMessage,
50+
assistantResponse: result.content,
51+
status,
52+
errored: status === 'error',
53+
aborted: status === 'aborted',
54+
errorMessage: result.error ?? result.errors?.join('\n'),
55+
mode: context.mode,
56+
source: 'workspace-chat',
57+
startedAt: new Date(context.startedAt).toISOString(),
58+
durationMs: Date.now() - context.startedAt,
59+
usage: {
60+
inputTokens: result.usage?.prompt ?? 0,
61+
outputTokens: result.usage?.completion ?? 0,
62+
},
63+
}
64+
65+
void fetch(url, {
66+
method: 'POST',
67+
headers,
68+
body: JSON.stringify({ input }),
69+
signal: AbortSignal.timeout(CHAT_LOG_TIMEOUT_MS),
70+
})
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+
})
87+
})
88+
}

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

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type ChatTurnLogContext, logChatTurn } from '@/lib/mothership/chat/chat-log'
34
import {
45
buildPersistedAssistantMessage,
56
withStoppedContentBlock,
@@ -23,6 +24,8 @@ export function buildOnComplete(params: {
2324
organizationId?: string
2425
userId?: string
2526
requestMode?: 'assistant' | 'agent' | 'plan'
27+
/** Present for Chat turns that feed the operator chat log. */
28+
chatLog?: ChatTurnLogContext
2629
/**
2730
* Root agent span for this request. When present, the final
2831
* assistant message + invoked tool calls are recorded as
@@ -46,6 +49,7 @@ export function buildOnComplete(params: {
4649
userId,
4750
runController,
4851
otelRoot,
52+
chatLog,
4953
} = params
5054
const notifyChatStatus = params.notifyChatStatus ?? params.notifyWorkspaceStatus ?? false
5155

@@ -78,6 +82,7 @@ export function buildOnComplete(params: {
7882
finalization.updated ||
7983
finalization.outcome === CopilotChatFinalizeOutcome.AssistantAlreadyPersisted
8084

85+
if (chatLog && shouldPublishCompletion) logChatTurn(chatLog, result, 'aborted')
8186
if (notifyChatStatus && shouldPublishCompletion) {
8287
publishChatStatusChanged(
8388
{ workspaceId, organizationId, userId },
@@ -107,6 +112,7 @@ export function buildOnComplete(params: {
107112
...(result.success ? {} : { streamMarkerPolicy: 'active-or-cleared' as const }),
108113
})
109114

115+
if (chatLog) logChatTurn(chatLog, result, result.success ? 'success' : 'error')
110116
if (notifyChatStatus) {
111117
publishChatStatusChanged(
112118
{ workspaceId, organizationId, userId },
@@ -138,9 +144,19 @@ export function buildOnError(params: {
138144
organizationId?: string
139145
userId?: string
140146
requestMode?: 'assistant' | 'agent' | 'plan'
147+
/** Present for Chat turns that feed the operator chat log. */
148+
chatLog?: ChatTurnLogContext
141149
}) {
142-
const { chatId, userMessageId, requestId, workspaceId, organizationId, userId, runController } =
143-
params
150+
const {
151+
chatId,
152+
userMessageId,
153+
requestId,
154+
workspaceId,
155+
organizationId,
156+
userId,
157+
runController,
158+
chatLog,
159+
} = params
144160
const notifyChatStatus = params.notifyChatStatus ?? params.notifyWorkspaceStatus ?? false
145161

146162
return async (error: Error, result?: OrchestratorResult) => {
@@ -151,18 +167,15 @@ export function buildOnError(params: {
151167
// cancelled / non-success completion path, so the partial assistant turn
152168
// (text + tool calls + subagent work) survives the refetch instead of the
153169
// chat collapsing to an empty assistant row.
154-
const assistantMessage = buildPersistedAssistantMessage(
155-
{
156-
content: '',
157-
contentBlocks: [],
158-
toolCalls: [],
159-
...result,
160-
success: false,
161-
error: result?.error || getErrorMessage(error),
162-
},
163-
requestId,
164-
params.requestMode
165-
)
170+
const failed: OrchestratorResult = {
171+
content: '',
172+
contentBlocks: [],
173+
toolCalls: [],
174+
...result,
175+
success: false,
176+
error: result?.error || getErrorMessage(error),
177+
}
178+
const assistantMessage = buildPersistedAssistantMessage(failed, requestId, params.requestMode)
166179
await finalizeAssistantTurn({
167180
runController,
168181
chatId,
@@ -171,6 +184,7 @@ export function buildOnError(params: {
171184
streamMarkerPolicy: 'active-or-cleared',
172185
})
173186

187+
if (chatLog) logChatTurn(chatLog, failed, 'error')
174188
if (notifyChatStatus) {
175189
publishChatStatusChanged(
176190
{ workspaceId, organizationId, userId },

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
type AssistantImageContent,
3737
prepareOrganizationChatAttachments,
3838
} from '@/lib/mothership/chat/assistant-images'
39+
import type { ChatTurnLogContext } from '@/lib/mothership/chat/chat-log'
3940
import { buildOnComplete, buildOnError } from '@/lib/mothership/chat/completion'
4041
import {
4142
DESKTOP_TERMINAL_HINT_ID_MAX_LENGTH,
@@ -985,6 +986,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
985986
let requestId = ''
986987
const executionId = generateId()
987988
const runId = generateId()
989+
const startedAt = Date.now()
988990

989991
try {
990992
const session = await getSession()
@@ -1485,6 +1487,22 @@ export async function handleUnifiedChatPost(req: NextRequest) {
14851487
// Admission committed. A failure to attach this HTTP sink must leave the turn recoverable.
14861488
sendClaim = undefined
14871489
}
1490+
const requestMode =
1491+
body.mode === 'plan' ? 'plan' : body.mode === 'assistant' ? 'assistant' : 'agent'
1492+
/** Workspace and organization Chat feed the operator chat log; the workflow panel does not. */
1493+
const chatLog: ChatTurnLogContext | undefined =
1494+
branch.kind !== 'workflow' && actualChatId
1495+
? {
1496+
chatId: actualChatId,
1497+
messageId: userMessageId,
1498+
requestId,
1499+
userId: authenticatedUserId,
1500+
...(authenticatedUserEmail ? { userEmail: authenticatedUserEmail } : {}),
1501+
userMessage: body.message,
1502+
mode: requestMode,
1503+
startedAt,
1504+
}
1505+
: undefined
14881506
const stream = createSSEStream({
14891507
requestPayload,
14901508
admittedRun,
@@ -1526,9 +1544,9 @@ export async function handleUnifiedChatPost(req: NextRequest) {
15261544
notifyChatStatus: branch.notifyChatStatus,
15271545
organizationId: branch.kind === 'organization' ? branch.organizationId : undefined,
15281546
userId: authenticatedUserId,
1529-
requestMode:
1530-
body.mode === 'plan' ? 'plan' : body.mode === 'assistant' ? 'assistant' : 'agent',
1547+
requestMode,
15311548
otelRoot,
1549+
chatLog,
15321550
}),
15331551
onError: buildOnError({
15341552
runController,
@@ -1539,8 +1557,8 @@ export async function handleUnifiedChatPost(req: NextRequest) {
15391557
notifyChatStatus: branch.notifyChatStatus,
15401558
organizationId: branch.kind === 'organization' ? branch.organizationId : undefined,
15411559
userId: authenticatedUserId,
1542-
requestMode:
1543-
body.mode === 'plan' ? 'plan' : body.mode === 'assistant' ? 'assistant' : 'agent',
1560+
requestMode,
1561+
chatLog,
15441562
}),
15451563
},
15461564
})

0 commit comments

Comments
 (0)