Skip to content

Commit 926ca54

Browse files
committed
checkpoint
1 parent a7a6b9a commit 926ca54

File tree

23 files changed

+15686
-102
lines changed

23 files changed

+15686
-102
lines changed

apps/sim/app/api/copilot/feedback/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ vi.mock('@sim/db/schema', () => ({
4444
feedbackId: 'feedbackId',
4545
userId: 'userId',
4646
chatId: 'chatId',
47+
requestId: 'requestId',
4748
userQuery: 'userQuery',
4849
agentResponse: 'agentResponse',
4950
isPositive: 'isPositive',

apps/sim/app/api/copilot/feedback/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const logger = createLogger('CopilotFeedbackAPI')
1818
// Schema for feedback submission
1919
const FeedbackSchema = z.object({
2020
chatId: z.string().uuid('Chat ID must be a valid UUID'),
21+
requestId: z.string().min(1).optional(),
2122
userQuery: z.string().min(1, 'User query is required'),
2223
agentResponse: z.string().min(1, 'Agent response is required'),
2324
isPositiveFeedback: z.boolean(),
@@ -42,12 +43,13 @@ export async function POST(req: NextRequest) {
4243
}
4344

4445
const body = await req.json()
45-
const { chatId, userQuery, agentResponse, isPositiveFeedback, feedback, workflowYaml } =
46+
const { chatId, requestId, userQuery, agentResponse, isPositiveFeedback, feedback, workflowYaml } =
4647
FeedbackSchema.parse(body)
4748

4849
logger.info(`[${tracker.requestId}] Processing copilot feedback submission`, {
4950
userId: authenticatedUserId,
5051
chatId,
52+
requestId,
5153
isPositiveFeedback,
5254
userQueryLength: userQuery.length,
5355
agentResponseLength: agentResponse.length,
@@ -62,6 +64,7 @@ export async function POST(req: NextRequest) {
6264
.values({
6365
userId: authenticatedUserId,
6466
chatId,
67+
requestId: requestId || null,
6568
userQuery,
6669
agentResponse,
6770
isPositive: isPositiveFeedback,
@@ -137,6 +140,7 @@ export async function GET(req: NextRequest) {
137140
feedbackId: copilotFeedback.feedbackId,
138141
userId: copilotFeedback.userId,
139142
chatId: copilotFeedback.chatId,
143+
requestId: copilotFeedback.requestId,
140144
userQuery: copilotFeedback.userQuery,
141145
agentResponse: copilotFeedback.agentResponse,
142146
isPositive: copilotFeedback.isPositive,

apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,15 @@ export function MessageActions({ content, chatId, userQuery, requestId }: Messag
121121
}
122122
submitFeedback.mutate({
123123
chatId,
124+
requestId,
124125
userQuery,
125126
agentResponse: content,
126127
isPositiveFeedback: pendingFeedback === 'up',
127128
feedback: text,
128129
})
129130
setPendingFeedback(null)
130131
setFeedbackText('')
131-
}, [pendingFeedback, chatId, userQuery, content, feedbackText])
132+
}, [pendingFeedback, chatId, requestId, userQuery, content, feedbackText])
132133

133134
const handleModalClose = useCallback((open: boolean) => {
134135
if (!open) {

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 114 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ const MAX_RECONNECT_ATTEMPTS = 10
169169
const RECONNECT_BASE_DELAY_MS = 1000
170170
const RECONNECT_MAX_DELAY_MS = 30_000
171171
const RECONNECT_EMPTY_BATCH_DELAY_MS = 500
172+
const RECONNECT_BATCH_REQUEST_TIMEOUT_MS = 15_000
173+
const RECONNECT_LIVE_TAIL_TIMEOUT_MS = 60_000
174+
const RECONNECT_RECOVERY_TIMEOUT_MS = 2 * 60_000
172175

173176
const logger = createLogger('useChat')
174177

@@ -194,6 +197,43 @@ function stringArrayParam(value: unknown): string[] {
194197
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
195198
}
196199

200+
function createTimedAbortSignal(
201+
parentSignal: AbortSignal | undefined,
202+
timeoutMs: number,
203+
timeoutMessage: string
204+
): {
205+
signal: AbortSignal
206+
didTimeout: () => boolean
207+
cleanup: () => void
208+
} {
209+
const controller = new AbortController()
210+
let timedOut = false
211+
212+
const onAbort = () => {
213+
controller.abort(parentSignal?.reason)
214+
}
215+
216+
if (parentSignal?.aborted) {
217+
controller.abort(parentSignal.reason)
218+
} else if (parentSignal) {
219+
parentSignal.addEventListener('abort', onAbort, { once: true })
220+
}
221+
222+
const timeoutId = setTimeout(() => {
223+
timedOut = true
224+
controller.abort(new Error(timeoutMessage))
225+
}, timeoutMs)
226+
227+
return {
228+
signal: controller.signal,
229+
didTimeout: () => timedOut,
230+
cleanup: () => {
231+
clearTimeout(timeoutId)
232+
parentSignal?.removeEventListener('abort', onAbort)
233+
},
234+
}
235+
}
236+
197237
function resolveWorkflowNameForDisplay(workflowId: unknown): string | undefined {
198238
const id = stringParam(workflowId)
199239
if (!id) return undefined
@@ -2296,21 +2336,36 @@ export function useChat(
22962336
afterCursor: string,
22972337
signal?: AbortSignal
22982338
): Promise<StreamBatchResponse> => {
2299-
const response = await fetch(
2300-
`/api/mothership/chat/stream?streamId=${encodeURIComponent(streamId)}&after=${encodeURIComponent(afterCursor)}&batch=true`,
2301-
{ signal }
2339+
const timeoutMessage = `Timed out fetching stream batch for ${streamId}`
2340+
const timedAbort = createTimedAbortSignal(
2341+
signal,
2342+
RECONNECT_BATCH_REQUEST_TIMEOUT_MS,
2343+
timeoutMessage
23022344
)
2303-
if (!response.ok) {
2304-
throw await createResumeTransportError(
2305-
response,
2306-
`Stream resume batch failed: ${response.status}`
2345+
try {
2346+
const response = await fetch(
2347+
`/api/mothership/chat/stream?streamId=${encodeURIComponent(streamId)}&after=${encodeURIComponent(afterCursor)}&batch=true`,
2348+
{ signal: timedAbort.signal }
23072349
)
2350+
if (!response.ok) {
2351+
throw await createResumeTransportError(
2352+
response,
2353+
`Stream resume batch failed: ${response.status}`
2354+
)
2355+
}
2356+
const batch = parseStreamBatchResponse(await response.json())
2357+
if (Array.isArray(batch.previewSessions) && batch.previewSessions.length > 0) {
2358+
seedPreviewSessions(batch.previewSessions)
2359+
}
2360+
return batch
2361+
} catch (error) {
2362+
if (timedAbort.didTimeout() && !signal?.aborted) {
2363+
throw new Error(timeoutMessage)
2364+
}
2365+
throw error
2366+
} finally {
2367+
timedAbort.cleanup()
23082368
}
2309-
const batch = parseStreamBatchResponse(await response.json())
2310-
if (Array.isArray(batch.previewSessions) && batch.previewSessions.length > 0) {
2311-
seedPreviewSessions(batch.previewSessions)
2312-
}
2313-
return batch
23142369
},
23152370
[seedPreviewSessions]
23162371
)
@@ -2328,6 +2383,7 @@ export function useChat(
23282383
let seedEvents = opts.initialBatch?.events ?? []
23292384
let streamStatus = opts.initialBatch?.status ?? 'unknown'
23302385
let streamError = opts.initialBatch?.error
2386+
const reconnectStartedAt = Date.now()
23312387

23322388
const isStaleReconnect = () =>
23332389
streamGenRef.current !== expectedGen || abortControllerRef.current?.signal.aborted === true
@@ -2341,6 +2397,15 @@ export function useChat(
23412397

23422398
try {
23432399
while (streamGenRef.current === expectedGen) {
2400+
if (Date.now() - reconnectStartedAt >= RECONNECT_RECOVERY_TIMEOUT_MS) {
2401+
logger.warn('Reconnect tail timed out waiting for terminal state', {
2402+
streamId,
2403+
afterCursor: latestCursor,
2404+
elapsedMs: Date.now() - reconnectStartedAt,
2405+
})
2406+
throw new Error(RECONNECT_TAIL_ERROR)
2407+
}
2408+
23442409
if (seedEvents.length > 0) {
23452410
const replayResult = await processSSEStreamRef.current(
23462411
buildReplayStream(seedEvents).getReader(),
@@ -2371,26 +2436,47 @@ export function useChat(
23712436

23722437
logger.info('Opening live stream tail', { streamId, afterCursor: latestCursor })
23732438

2374-
const sseRes = await fetch(
2375-
`/api/mothership/chat/stream?streamId=${encodeURIComponent(streamId)}&after=${encodeURIComponent(latestCursor)}`,
2376-
{ signal: activeAbort.signal }
2439+
const tailTimeoutMessage = `Timed out waiting for live reconnect tail for ${streamId}`
2440+
const timedTailAbort = createTimedAbortSignal(
2441+
activeAbort.signal,
2442+
RECONNECT_LIVE_TAIL_TIMEOUT_MS,
2443+
tailTimeoutMessage
23772444
)
2378-
if (!sseRes.ok || !sseRes.body) {
2379-
throw await createResumeTransportError(sseRes, RECONNECT_TAIL_ERROR)
2380-
}
2445+
let liveResult: { sawStreamError: boolean; sawComplete: boolean }
2446+
try {
2447+
const sseRes = await fetch(
2448+
`/api/mothership/chat/stream?streamId=${encodeURIComponent(streamId)}&after=${encodeURIComponent(latestCursor)}`,
2449+
{ signal: timedTailAbort.signal }
2450+
)
2451+
if (!sseRes.ok || !sseRes.body) {
2452+
throw await createResumeTransportError(sseRes, RECONNECT_TAIL_ERROR)
2453+
}
23812454

2382-
if (isStaleReconnect()) {
2383-
return { error: false, aborted: true }
2384-
}
2455+
if (isStaleReconnect()) {
2456+
return { error: false, aborted: true }
2457+
}
23852458

2386-
setTransportStreaming()
2459+
setTransportStreaming()
23872460

2388-
const liveResult = await processSSEStreamRef.current(
2389-
sseRes.body.getReader(),
2390-
assistantId,
2391-
expectedGen,
2392-
{ preserveExistingState: true }
2393-
)
2461+
liveResult = await processSSEStreamRef.current(
2462+
sseRes.body.getReader(),
2463+
assistantId,
2464+
expectedGen,
2465+
{ preserveExistingState: true }
2466+
)
2467+
} catch (error) {
2468+
if (timedTailAbort.didTimeout() && !activeAbort.signal.aborted) {
2469+
logger.warn('Live reconnect tail request timed out', {
2470+
streamId,
2471+
afterCursor: latestCursor,
2472+
timeoutMs: RECONNECT_LIVE_TAIL_TIMEOUT_MS,
2473+
})
2474+
throw new Error(RECONNECT_TAIL_ERROR)
2475+
}
2476+
throw error
2477+
} finally {
2478+
timedTailAbort.cleanup()
2479+
}
23942480

23952481
if (liveResult.sawStreamError) {
23962482
return { error: true, aborted: false }

apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ function TracesTab({ environment }: { environment: MothershipEnv }) {
605605
value={requestIdInput}
606606
onChange={(e) => setRequestIdInput(e.target.value)}
607607
onKeyDown={(e) => e.key === 'Enter' && handleLookup()}
608-
placeholder='Paste a request ID (sim_request_id)...'
608+
placeholder='Paste a request ID...'
609609
className='font-mono text-[13px]'
610610
/>
611611
<Button variant='primary' onClick={handleLookup} disabled={!requestIdInput.trim()}>
@@ -644,8 +644,8 @@ interface TraceSpan {
644644

645645
interface TraceData {
646646
id: string
647-
simRequestId: string
648-
goTraceId: string
647+
requestId: string
648+
goTraceId?: string
649649
streamId?: string
650650
chatId?: string
651651
userId?: string
@@ -686,8 +686,8 @@ function TraceDetail({ trace }: { trace: TraceData }) {
686686
<div className='flex flex-col gap-4'>
687687
{/* Trace metadata */}
688688
<div className='grid grid-cols-2 gap-x-6 gap-y-2 rounded-md border border-[var(--border-secondary)] p-4'>
689-
<MetaRow label='Go Trace ID' value={trace.goTraceId} mono />
690-
<MetaRow label='Sim Request ID' value={trace.simRequestId} mono />
689+
<MetaRow label='Request ID' value={trace.requestId} mono />
690+
<MetaRow label='Go Trace ID' value={trace.goTraceId || '—'} mono />
691691
<MetaRow label='Outcome'>
692692
<Badge
693693
variant={

apps/sim/hooks/queries/copilot-feedback.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const logger = createLogger('CopilotFeedbackMutation')
55

66
interface SubmitFeedbackVariables {
77
chatId: string
8+
requestId?: string
89
userQuery: string
910
agentResponse: string
1011
isPositiveFeedback: boolean

apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = {
17431743
"MothershipStreamV1Trace": {
17441744
"additionalProperties": false,
17451745
"properties": {
1746+
"goTraceId": {
1747+
"type": "string"
1748+
},
17461749
"requestId": {
17471750
"type": "string"
17481751
},

apps/sim/lib/copilot/generated/mothership-stream-v1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface MothershipStreamV1StreamRef {
6666
streamId: string;
6767
}
6868
export interface MothershipStreamV1Trace {
69+
goTraceId?: string;
6970
requestId: string;
7071
spanId?: string;
7172
}

apps/sim/lib/copilot/generated/request-trace-v1.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export interface RequestTraceV1SimReport {
2828
executionId?: string;
2929
goTraceId?: string;
3030
outcome: RequestTraceV1Outcome;
31+
requestId: string;
3132
runId?: string;
32-
simRequestId: string;
3333
spans: RequestTraceV1Span[];
3434
startMs: number;
3535
streamId?: string;
@@ -84,10 +84,10 @@ export interface RequestTraceV1MergedTrace {
8484
cost?: RequestTraceV1CostSummary;
8585
durationMs: number;
8686
endMs: number;
87-
goTraceId: string;
87+
goTraceId?: string;
8888
outcome: RequestTraceV1Outcome;
89+
requestId: string;
8990
serviceCharges?: MothershipStreamV1AdditionalPropertiesMap;
90-
simRequestId?: string;
9191
spans: RequestTraceV1Span[];
9292
startMs: number;
9393
streamId?: string;
@@ -106,8 +106,8 @@ export interface RequestTraceV1SimReport1 {
106106
executionId?: string;
107107
goTraceId?: string;
108108
outcome: RequestTraceV1Outcome;
109+
requestId: string;
109110
runId?: string;
110-
simRequestId: string;
111111
spans: RequestTraceV1Span[];
112112
startMs: number;
113113
streamId?: string;

apps/sim/lib/copilot/generated/tool-catalog-v1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ export const Run: ToolCatalogEntry = {
616616
name: "run",
617617
route: "subagent",
618618
mode: "async",
619-
parameters: {"properties":{"context":{"description":"Pre-gathered context: workflow state, block IDs, input requirements.","type":"string"},"request":{"description":"What to run or what execution results to inspect.","type":"string"}},"required":["request"],"type":"object"},
619+
parameters: {"properties":{"context":{"description":"Pre-gathered context: workflow state, block IDs, input requirements.","type":"string"},"request":{"description":"What to run or what logs to check.","type":"string"}},"required":["request"],"type":"object"},
620620
subagentId: "run",
621621
internal: true,
622622
};

0 commit comments

Comments
 (0)