11import { spawn } from 'node:child_process' ;
2- import { closeSync , existsSync , openSync } from 'node:fs' ;
2+ import { closeSync , existsSync , openSync , statSync } from 'node:fs' ;
33import { mkdir , readFile , rm , writeFile } from 'node:fs/promises' ;
44import path from 'node:path' ;
55import { NextResponse } from 'next/server' ;
@@ -12,6 +12,12 @@ type AgentationWebhookPayload = {
1212 id ?: string ;
1313 comment ?: string ;
1414 sessionId ?: string ;
15+ timestamp ?: number ;
16+ url ?: string ;
17+ element ?: string ;
18+ elementPath ?: string ;
19+ nearbyText ?: string ;
20+ selectedText ?: string ;
1521 } ;
1622 timestamp ?: number ;
1723 url ?: string ;
@@ -28,8 +34,42 @@ const AUTO_EVENTS = new Set(['annotation.add', 'submit']);
2834const AUTORUN_DIR = path . join ( process . cwd ( ) , '.agentation' ) ;
2935const LOCK_PATH = path . join ( AUTORUN_DIR , 'autorun.lock.json' ) ;
3036const LOG_PATH = path . join ( AUTORUN_DIR , 'autorun.log' ) ;
37+ const AUTORUN_MAX_AGE_MS = Number (
38+ process . env . AGENTATION_AUTORUN_MAX_AGE_MS ?? '120000'
39+ ) ;
40+ const AUTORUN_IDLE_TIMEOUT_MS = Number (
41+ process . env . AGENTATION_AUTORUN_IDLE_TIMEOUT_MS ?? '45000'
42+ ) ;
3143const DEFAULT_PROMPT =
32- 'watch mode로 계속 처리해줘. Agentation pending annotation을 확인해서 코드 반영, 테스트 검증, resolve 처리까지 완료해줘.' ;
44+ '방금 등록된 Agentation annotation 한 건을 처리해줘. webhook에 담긴 코멘트와 위치 정보를 기준으로 관련 코드만 최소 수정하고, 필요한 테스트만 검증한 뒤 resolve 처리하고 바로 종료해줘. 브라우저를 새로 열거나 추가 입력을 기다리거나 watch mode로 머물지 말아줘. pending 조회에서 바로 안 보여도 아래 정보를 기준으로 해당 요청을 찾아 처리해줘.' ;
45+
46+ function buildAutorunPrompt ( payload : AgentationWebhookPayload ) : string {
47+ const annotation = payload . annotation ;
48+ const details = [
49+ annotation ?. id ? `- webhook annotation id: ${ annotation . id } ` : null ,
50+ annotation ?. comment ? `- comment: ${ annotation . comment } ` : null ,
51+ annotation ?. sessionId ? `- sessionId: ${ annotation . sessionId } ` : null ,
52+ annotation ?. url || payload . url
53+ ? `- url: ${ annotation ?. url ?? payload . url } `
54+ : null ,
55+ annotation ?. element ? `- element: ${ annotation . element } ` : null ,
56+ annotation ?. elementPath ? `- elementPath: ${ annotation . elementPath } ` : null ,
57+ annotation ?. nearbyText ? `- nearbyText: ${ annotation . nearbyText } ` : null ,
58+ annotation ?. selectedText
59+ ? `- selectedText: ${ annotation . selectedText } `
60+ : null ,
61+ annotation ?. timestamp
62+ ? `- annotation timestamp: ${ annotation . timestamp } `
63+ : null ,
64+ payload . timestamp ? `- event timestamp: ${ payload . timestamp } ` : null ,
65+ ] . filter ( ( value ) : value is string => Boolean ( value ) ) ;
66+
67+ if ( details . length === 0 ) {
68+ return DEFAULT_PROMPT ;
69+ }
70+
71+ return `${ DEFAULT_PROMPT } \n\n다음 정보를 참고해줘:\n${ details . join ( '\n' ) } ` ;
72+ }
3373
3474function isPidRunning ( pid : number ) : boolean {
3575 try {
@@ -40,6 +80,24 @@ function isPidRunning(pid: number): boolean {
4080 }
4181}
4282
83+ function getLockAgeMs ( lock : AutorunLock ) : number {
84+ const startedAt = new Date ( lock . startedAt ) . getTime ( ) ;
85+
86+ if ( Number . isNaN ( startedAt ) ) {
87+ return Number . POSITIVE_INFINITY ;
88+ }
89+
90+ return Date . now ( ) - startedAt ;
91+ }
92+
93+ function getAutorunIdleMs ( ) : number {
94+ try {
95+ return Date . now ( ) - statSync ( LOG_PATH ) . mtimeMs ;
96+ } catch {
97+ return Number . POSITIVE_INFINITY ;
98+ }
99+ }
100+
43101async function readLockFile ( ) : Promise < AutorunLock | null > {
44102 if ( ! existsSync ( LOCK_PATH ) ) {
45103 return null ;
@@ -61,10 +119,21 @@ async function clearStaleLock(lock: AutorunLock | null): Promise<void> {
61119 if ( ! lock ) {
62120 return ;
63121 }
64- if ( isPidRunning ( lock . pid ) ) {
122+
123+ const isRunning = isPidRunning ( lock . pid ) ;
124+ const isExpired = getLockAgeMs ( lock ) > AUTORUN_MAX_AGE_MS ;
125+ const isIdle = getAutorunIdleMs ( ) > AUTORUN_IDLE_TIMEOUT_MS ;
126+
127+ if ( isRunning && ! isExpired && ! isIdle ) {
65128 return ;
66129 }
67130
131+ if ( isRunning && ( isExpired || isIdle ) ) {
132+ try {
133+ process . kill ( lock . pid , 'SIGTERM' ) ;
134+ } catch { }
135+ }
136+
68137 if ( existsSync ( LOCK_PATH ) ) {
69138 await rm ( LOCK_PATH , { force : true } ) ;
70139 }
@@ -74,7 +143,7 @@ async function writeLockFile(lock: AutorunLock): Promise<void> {
74143 await writeFile ( LOCK_PATH , JSON . stringify ( lock , null , 2 ) , 'utf8' ) ;
75144}
76145
77- function runAutorunCommand ( annotationId ?: string ) {
146+ function runAutorunCommand ( payload : AgentationWebhookPayload ) {
78147 const customCommand = process . env . AGENTATION_AUTORUN_COMMAND ?. trim ( ) ;
79148 const logFd = openSync ( LOG_PATH , 'a' ) ;
80149 try {
@@ -88,9 +157,7 @@ function runAutorunCommand(annotationId?: string) {
88157 } ) ;
89158 }
90159
91- const autoPrompt = annotationId
92- ? `${ DEFAULT_PROMPT } 방금 등록된 annotation id는 ${ annotationId } 예요.`
93- : DEFAULT_PROMPT ;
160+ const autoPrompt = buildAutorunPrompt ( payload ) ;
94161
95162 return spawn (
96163 'codex' ,
@@ -171,7 +238,7 @@ export async function POST(request: Request) {
171238 } ) ;
172239 }
173240
174- const child = runAutorunCommand ( payload . annotation ?. id ) ;
241+ const child = runAutorunCommand ( payload ) ;
175242 if ( ! child . pid ) {
176243 return NextResponse . json (
177244 { ok : false , reason : 'failed to spawn autorun process' } ,
0 commit comments