@@ -17,8 +17,8 @@ import { fetchSessionSnapshot } from '@/lib/session-ingest-client';
1717import { trackSecurityAgentAnalysisCompleted } from '@/lib/security-agent/posthog-tracking' ;
1818import { generateApiToken } from '@/lib/tokens' ;
1919import { db } from '@/lib/drizzle' ;
20- import { kilocode_users } from '@kilocode/db/schema' ;
21- import { eq } from 'drizzle-orm' ;
20+ import { kilocode_users , security_analysis_queue } from '@kilocode/db/schema' ;
21+ import { and , eq , inArray } from 'drizzle-orm' ;
2222import { z } from 'zod' ;
2323import { verifyCallbackToken } from '@kilocode/worker-utils/callback-token' ;
2424import { logExceptInTest , sentryLogger } from '@/lib/utils.server' ;
@@ -32,6 +32,8 @@ import {
3232 DEFAULT_SECURITY_AGENT_TRIAGE_MODEL ,
3333} from '@/lib/security-agent/core/constants' ;
3434
35+ // Compatibility-only callback ingress retained for explicit rollback routing.
36+ // Durable default ingress lives in the security-auto-analysis Worker.
3537const log = sentryLogger ( 'security-agent:callback' , 'info' ) ;
3638const warn = sentryLogger ( 'security-agent:callback' , 'warning' ) ;
3739const logError = sentryLogger ( 'security-agent:callback' , 'error' ) ;
@@ -82,14 +84,18 @@ export async function POST(
8284) {
8385 try {
8486 const { findingId } = await params ;
87+ const attemptToken = req . nextUrl . searchParams . get ( 'attempt' ) ;
88+ if ( ! attemptToken ) {
89+ return NextResponse . json ( { error : 'Missing callback attempt token' } , { status : 400 } ) ;
90+ }
8591 const callbackToken = req . headers . get ( 'X-Callback-Token' ) ;
8692 const validCallbackToken =
8793 ! ! CALLBACK_TOKEN_SECRET &&
8894 ( await verifyCallbackToken ( {
8995 token : callbackToken ,
9096 secret : CALLBACK_TOKEN_SECRET ,
9197 scope : 'security-analysis-callback' ,
92- resourceParts : [ findingId ] ,
98+ resourceParts : [ findingId , attemptToken ] ,
9399 } ) ) ;
94100 if ( ! validCallbackToken ) {
95101 return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } ) ;
@@ -117,6 +123,28 @@ export async function POST(
117123 return NextResponse . json ( { error : 'Finding not found' } , { status : 404 } ) ;
118124 }
119125
126+ const [ activeAttempt ] = await db
127+ . select ( { claimToken : security_analysis_queue . claim_token } )
128+ . from ( security_analysis_queue )
129+ . where (
130+ and (
131+ eq ( security_analysis_queue . finding_id , findingId ) ,
132+ inArray ( security_analysis_queue . queue_status , [ 'pending' , 'running' ] )
133+ )
134+ )
135+ . limit ( 1 ) ;
136+ if (
137+ ( finding . analysis_status === 'pending' || finding . analysis_status === 'running' ) &&
138+ activeAttempt ?. claimToken !== attemptToken
139+ ) {
140+ warn ( 'Ignoring stale auto-analysis callback due to attempt mismatch' , {
141+ findingId,
142+ callbackAttemptToken : attemptToken ,
143+ activeAttemptToken : activeAttempt ?. claimToken ?? null ,
144+ } ) ;
145+ return NextResponse . json ( { success : true , message : 'Stale callback ignored' } ) ;
146+ }
147+
120148 const sessionMismatch =
121149 ( payload . cloudAgentSessionId &&
122150 finding . session_id &&
@@ -158,6 +186,7 @@ export async function POST(
158186 } ) ;
159187 await transitionAutoAnalysisQueueFromCallback ( {
160188 findingId,
189+ attemptToken,
161190 toStatus : 'completed' ,
162191 failureCode : 'SKIPPED_NO_LONGER_ELIGIBLE' ,
163192 } ) ;
@@ -184,9 +213,9 @@ export async function POST(
184213 after ( async ( ) => {
185214 try {
186215 if ( payload . status === 'completed' ) {
187- await handleAnalysisCompleted ( findingId , payload , finding ) ;
216+ await handleAnalysisCompleted ( findingId , attemptToken , payload , finding ) ;
188217 } else if ( payload . status === 'failed' || payload . status === 'interrupted' ) {
189- await handleAnalysisFailed ( findingId , payload , finding ) ;
218+ await handleAnalysisFailed ( findingId , attemptToken , payload , finding ) ;
190219 } else {
191220 const unknownStatus = payload . status as string ;
192221 logError ( 'Unknown callback status received, marking as failed' , {
@@ -202,6 +231,7 @@ export async function POST(
202231 }
203232 await transitionAutoAnalysisQueueFromCallback ( {
204233 findingId,
234+ attemptToken,
205235 toStatus : 'failed' ,
206236 failureCode : 'STATE_GUARD_REJECTED' ,
207237 errorMessage : `Unknown callback status: ${ unknownStatus } ` ,
@@ -255,6 +285,7 @@ function readAnalysisContext(analysis: SecurityFindingAnalysis | null | undefine
255285
256286async function handleAnalysisCompleted (
257287 findingId : string ,
288+ attemptToken : string ,
258289 payload : ExecutionCallbackPayload ,
259290 finding : Awaited < ReturnType < typeof getSecurityFindingById > > & { }
260291) {
@@ -281,6 +312,7 @@ async function handleAnalysisCompleted(
281312 }
282313 await transitionAutoAnalysisQueueFromCallback ( {
283314 findingId,
315+ attemptToken,
284316 toStatus : 'failed' ,
285317 failureCode : 'STATE_GUARD_REJECTED' ,
286318 errorMessage : 'Cannot process callback — triggeredByUserId missing from analysis context' ,
@@ -300,6 +332,7 @@ async function handleAnalysisCompleted(
300332 }
301333 await transitionAutoAnalysisQueueFromCallback ( {
302334 findingId,
335+ attemptToken,
303336 toStatus : 'failed' ,
304337 failureCode : 'STATE_GUARD_REJECTED' ,
305338 errorMessage : 'Callback missing kiloSessionId — cannot retrieve analysis result' ,
@@ -365,6 +398,7 @@ async function handleAnalysisCompleted(
365398 }
366399 await transitionAutoAnalysisQueueFromCallback ( {
367400 findingId,
401+ attemptToken,
368402 toStatus : 'failed' ,
369403 failureCode : 'START_CALL_AMBIGUOUS' ,
370404 errorMessage : 'Analysis completed but result could not be retrieved from ingest service' ,
@@ -404,6 +438,7 @@ async function handleAnalysisCompleted(
404438 }
405439 await transitionAutoAnalysisQueueFromCallback ( {
406440 findingId,
441+ attemptToken,
407442 toStatus : 'failed' ,
408443 failureCode : 'STATE_GUARD_REJECTED' ,
409444 errorMessage : `User ${ triggeredByUserId } not found — cannot run Tier 3 extraction` ,
@@ -449,6 +484,7 @@ async function handleAnalysisCompleted(
449484 } ) ;
450485 await transitionAutoAnalysisQueueFromCallback ( {
451486 findingId,
487+ attemptToken,
452488 toStatus : 'failed' ,
453489 failureCode : 'START_CALL_AMBIGUOUS' ,
454490 errorMessage : error instanceof Error ? error . message : String ( error ) ,
@@ -458,17 +494,23 @@ async function handleAnalysisCompleted(
458494
459495 const updatedFinding = await getSecurityFindingById ( findingId ) ;
460496 if ( updatedFinding ?. analysis_status === 'completed' ) {
461- await transitionAutoAnalysisQueueFromCallback ( { findingId, toStatus : 'completed' } ) ;
497+ await transitionAutoAnalysisQueueFromCallback ( {
498+ findingId,
499+ attemptToken,
500+ toStatus : 'completed' ,
501+ } ) ;
462502 } else if ( updatedFinding ?. analysis_status === 'failed' ) {
463503 await transitionAutoAnalysisQueueFromCallback ( {
464504 findingId,
505+ attemptToken,
465506 toStatus : 'failed' ,
466507 failureCode : 'START_CALL_AMBIGUOUS' ,
467508 errorMessage : updatedFinding . analysis_error ?? undefined ,
468509 } ) ;
469510 } else {
470511 await transitionAutoAnalysisQueueFromCallback ( {
471512 findingId,
513+ attemptToken,
472514 toStatus : 'failed' ,
473515 failureCode : 'STATE_GUARD_REJECTED' ,
474516 errorMessage : `Unexpected post-finalize state: ${ updatedFinding ?. analysis_status ?? 'finding_not_found' } ` ,
@@ -478,6 +520,7 @@ async function handleAnalysisCompleted(
478520
479521async function handleAnalysisFailed (
480522 findingId : string ,
523+ attemptToken : string ,
481524 payload : ExecutionCallbackPayload ,
482525 finding : Awaited < ReturnType < typeof getSecurityFindingById > > & { }
483526) {
@@ -521,6 +564,7 @@ async function handleAnalysisFailed(
521564 }
522565 await transitionAutoAnalysisQueueFromCallback ( {
523566 findingId,
567+ attemptToken,
524568 toStatus : 'failed' ,
525569 failureCode : callbackFailure . failureCode ,
526570 errorMessage,
0 commit comments