aiexecode 1.0.72 → 1.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

@@ -0,0 +1,133 @@
1
+ /**
2
+ * 대화 히스토리 trim 및 정리 유틸리티
3
+ *
4
+ * orchestrator와 completion_judge에서 공통으로 사용하는
5
+ * 대화 관리 기능을 중앙집중화합니다.
6
+ */
7
+
8
+ import { createDebugLogger } from "../util/debug_log.js";
9
+
10
+ const debugLog = createDebugLogger('conversation_trimmer.log', 'conversation_trimmer');
11
+
12
+ /**
13
+ * 대화에서 특정 call_id와 관련된 모든 항목을 제거합니다.
14
+ *
15
+ * @param {Array} conversation - 대화 배열
16
+ * @param {string} callIdToRemove - 제거할 call_id
17
+ */
18
+ export function removeEntriesWithCallId(conversation, callIdToRemove) {
19
+ if (!callIdToRemove || typeof callIdToRemove !== 'string') {
20
+ return;
21
+ }
22
+
23
+ debugLog(`[removeEntriesWithCallId] Looking for entries with call_id: ${callIdToRemove}`);
24
+
25
+ // 1단계: 삭제할 항목들의 인덱스를 먼저 수집 (반복 중에는 배열 수정 금지)
26
+ const indicesToRemove = [];
27
+
28
+ for (let i = 0; i < conversation.length; i++) {
29
+ const entry = conversation[i];
30
+
31
+ // call_id 속성이 있고 값이 일치하면 삭제 목록에 추가
32
+ if (entry && entry.call_id === callIdToRemove) {
33
+ indicesToRemove.push(i);
34
+ debugLog(`[removeEntriesWithCallId] Marked index ${i} for removal (type: ${entry.type})`);
35
+ }
36
+ }
37
+
38
+ // 2단계: 수집된 인덱스를 역순으로 삭제 (뒤에서부터 지워야 인덱스가 꼬이지 않음)
39
+ for (let i = indicesToRemove.length - 1; i >= 0; i--) {
40
+ const indexToRemove = indicesToRemove[i];
41
+ conversation.splice(indexToRemove, 1);
42
+ debugLog(`[removeEntriesWithCallId] Removed entry at index ${indexToRemove}`);
43
+ }
44
+
45
+ debugLog(`[removeEntriesWithCallId] Total removed: ${indicesToRemove.length} entries`);
46
+ }
47
+
48
+ /**
49
+ * 고아(orphan) function_call_output을 제거합니다.
50
+ *
51
+ * 고아 output이란?
52
+ * - function_call은 없는데 function_call_output만 남아있는 경우
53
+ * - 예: function_call을 trim했는데 output은 뒤에 남아있는 경우
54
+ *
55
+ * @param {Array} conversation - 대화 배열
56
+ */
57
+ export function cleanupOrphanOutputs(conversation) {
58
+ debugLog(`[cleanupOrphanOutputs] Starting cleanup, conversation length: ${conversation.length}`);
59
+
60
+ // 1단계: "어떤 function_call들이 대화에 있는가?" 파악
61
+ const existingCallIds = new Set();
62
+
63
+ for (const entry of conversation) {
64
+ if (entry?.type === 'function_call' && entry.call_id) {
65
+ existingCallIds.add(entry.call_id);
66
+ }
67
+ }
68
+
69
+ debugLog(`[cleanupOrphanOutputs] Found ${existingCallIds.size} function_calls`);
70
+
71
+ // 2단계: "고아 output 찾기" - call_id가 없거나, 대응하는 function_call이 없는 output
72
+ const orphanIndexes = [];
73
+
74
+ for (let i = 0; i < conversation.length; i++) {
75
+ const entry = conversation[i];
76
+
77
+ // output이 아니면 패스
78
+ if (entry?.type !== 'function_call_output') {
79
+ continue;
80
+ }
81
+
82
+ const outputCallId = entry.call_id;
83
+
84
+ // call_id가 없거나, 대응하는 function_call이 없으면 고아
85
+ const isOrphan = !outputCallId || !existingCallIds.has(outputCallId);
86
+
87
+ if (isOrphan) {
88
+ orphanIndexes.push(i);
89
+ debugLog(`[cleanupOrphanOutputs] Found orphan at index ${i}, call_id: ${outputCallId}`);
90
+ }
91
+ }
92
+
93
+ // 3단계: 고아들을 뒤에서부터 제거 (앞에서부터 지우면 인덱스가 밀림)
94
+ for (let i = orphanIndexes.length - 1; i >= 0; i--) {
95
+ const indexToRemove = orphanIndexes[i];
96
+ conversation.splice(indexToRemove, 1);
97
+ }
98
+
99
+ debugLog(`[cleanupOrphanOutputs] Removed ${orphanIndexes.length} orphans. New length: ${conversation.length}`);
100
+ }
101
+
102
+ /**
103
+ * 컨텍스트 윈도우 초과 시 대화를 줄입니다.
104
+ * 시스템 프롬프트(인덱스 0)를 제외한 가장 오래된 항목(인덱스 1)을 제거합니다.
105
+ *
106
+ * @param {Array} conversation - 대화 배열
107
+ * @returns {boolean} 제거 성공 여부
108
+ */
109
+ export function trimConversation(conversation) {
110
+ // 시스템 프롬프트와 최소한 하나의 항목이 필요
111
+ if (conversation.length <= 2) {
112
+ debugLog(`[trimConversation] Cannot trim - conversation too short (${conversation.length} entries)`);
113
+ return false;
114
+ }
115
+
116
+ // 인덱스 1 (시스템 프롬프트 제외 첫 번째 항목) 선택
117
+ const targetEntry = conversation[1];
118
+ const targetCallId = targetEntry?.call_id;
119
+
120
+ debugLog(`[trimConversation] Target entry at index 1: type=${targetEntry?.type}, call_id=${targetCallId}`);
121
+
122
+ // 먼저 인덱스 1 항목 제거
123
+ conversation.splice(1, 1);
124
+ debugLog(`[trimConversation] Removed entry at index 1`);
125
+
126
+ // call_id가 있으면 같은 call_id를 가진 다른 항목들도 제거
127
+ if (targetCallId) {
128
+ removeEntriesWithCallId(conversation, targetCallId);
129
+ }
130
+
131
+ debugLog(`[trimConversation] Trim completed. New conversation length: ${conversation.length}`);
132
+ return true;
133
+ }
@@ -43,8 +43,7 @@ async function willDefinitelyFail(toolName, args) {
43
43
  try {
44
44
  const absolutePath = resolve(args.file_path);
45
45
 
46
- // 스냅샷만 확인 (실제 파일을 읽지 않음)
47
- // 편집 도구는 반드시 먼저 read_file로 읽어야 함
46
+ // 스냅샷 확인 (파일을 먼저 read_file로 읽었는지 확인)
48
47
  const snapshot = getFileSnapshot(absolutePath);
49
48
  const content = snapshot?.content;
50
49
 
@@ -53,7 +52,7 @@ async function willDefinitelyFail(toolName, args) {
53
52
  return { willFail: true, reason: 'File not read yet or empty' };
54
53
  }
55
54
 
56
- // old_string이 파일에 없는 경우
55
+ // old_string이 스냅샷에 없는 경우
57
56
  const oldString = args.old_string || '';
58
57
  if (!content.includes(oldString)) {
59
58
  return { willFail: true, reason: 'old_string not found in file' };
@@ -64,13 +63,12 @@ async function willDefinitelyFail(toolName, args) {
64
63
  }
65
64
  }
66
65
 
67
- // edit_file_range: 스냅샷이 없는 경우
66
+ // edit_file_range: 스냅샷이 없거나 라인 범위가 잘못된 경우
68
67
  if (toolName === 'edit_file_range') {
69
68
  try {
70
69
  const absolutePath = resolve(args.file_path);
71
70
 
72
- // 스냅샷만 확인 (실제 파일을 읽지 않음)
73
- // 편집 도구는 반드시 먼저 read_file로 읽어야 함
71
+ // 스냅샷 확인 (파일을 먼저 read_file로 읽었는지 확인)
74
72
  const snapshot = getFileSnapshot(absolutePath);
75
73
  const content = snapshot?.content;
76
74
 
@@ -78,6 +76,19 @@ async function willDefinitelyFail(toolName, args) {
78
76
  if (content === undefined || content === null) {
79
77
  return { willFail: true, reason: 'File not read yet or empty' };
80
78
  }
79
+
80
+ // 라인 범위 검증
81
+ const lines = content.split('\n');
82
+ const totalLines = content === '' ? 0 :
83
+ (content.endsWith('\n') ? lines.length - 1 : lines.length);
84
+
85
+ if (args.start_line < 1 || args.end_line < args.start_line - 1) {
86
+ return { willFail: true, reason: 'Invalid line range' };
87
+ }
88
+
89
+ if (args.start_line > totalLines + 1 || args.end_line > totalLines) {
90
+ return { willFail: true, reason: `Line range exceeds file length (${totalLines} lines)` };
91
+ }
81
92
  } catch (error) {
82
93
  return { willFail: true, reason: `Validation error: ${error.message}` };
83
94
  }
@@ -442,7 +442,6 @@ export async function edit_file_replace({ file_path, old_string, new_string }) {
442
442
  return {
443
443
  operation_successful: true,
444
444
  target_file_path: absolutePath,
445
- absolute_file_path: absolutePath,
446
445
  replacement_count: replacementCount,
447
446
  fileSnapshot: fileSnapshot, // UI 히스토리에서 정확한 diff 표시를 위해 편집 전 스냅샷 포함
448
447
  file_stats: {
@@ -654,7 +653,6 @@ export async function edit_file_range({ file_path, start_line, end_line, new_con
654
653
  operation_successful: true,
655
654
  operation_type: operationType,
656
655
  target_file_path: absolutePath,
657
- absolute_file_path: absolutePath,
658
656
  fileSnapshot: fileSnapshot, // UI 히스토리에서 정확한 diff 표시를 위해 편집 전 스냅샷 포함
659
657
  file_stats: {
660
658
  updated_content: newFileContent // 수정 후 전체 파일 내용
@@ -1,5 +1,6 @@
1
1
  import { safeReadFile } from '../util/safe_fs.js';
2
2
  import { resolve } from 'path';
3
+ import { createHash } from 'crypto';
3
4
  import { trackFileRead, saveFileSnapshot } from '../system/file_integrity.js';
4
5
  import { createDebugLogger } from '../util/debug_log.js';
5
6
  import { toDisplayPath } from '../util/path_helper.js';
@@ -77,6 +78,11 @@ export async function read_file({ filePath }) {
77
78
  saveFileSnapshot(absolutePath, content);
78
79
  debugLog(`Snapshot saved`);
79
80
 
81
+ // MD5 해시 계산
82
+ debugLog(`Calculating MD5 hash...`);
83
+ const md5Hash = createHash('md5').update(content).digest('hex');
84
+ debugLog(`MD5 hash: ${md5Hash}`);
85
+
80
86
  debugLog('========== read_file SUCCESS END ==========');
81
87
 
82
88
  return {
@@ -84,8 +90,7 @@ export async function read_file({ filePath }) {
84
90
  target_file_path: absolutePath,
85
91
  total_line_count: totalLines,
86
92
  file_content: content,
87
- file_lines: content === '' ? [] :
88
- (content.endsWith('\n') ? lines.slice(0, -1) : lines)
93
+ md5_hash: md5Hash
89
94
  };
90
95
  } catch (error) {
91
96
  debugLog(`========== read_file EXCEPTION ==========`);
@@ -200,6 +205,11 @@ export async function read_file_range({ filePath, startLine, endLine }) {
200
205
  saveFileSnapshot(absolutePath, content);
201
206
  debugLog(`Snapshot saved`);
202
207
 
208
+ // MD5 해시 계산 (전체 파일 내용에 대해)
209
+ debugLog(`Calculating MD5 hash...`);
210
+ const md5Hash = createHash('md5').update(content).digest('hex');
211
+ debugLog(`MD5 hash: ${md5Hash}`);
212
+
203
213
  // 라인 번호 유효성 검사 (1부터 시작)
204
214
  debugLog(`Validating line range...`);
205
215
  if (startLine < 1) {
@@ -225,16 +235,6 @@ export async function read_file_range({ filePath, startLine, endLine }) {
225
235
  const selectedLines = actualLines.slice(startLine - 1, endLine);
226
236
  debugLog(`Extracted ${selectedLines.length} lines`);
227
237
 
228
- const numberedLines = selectedLines.map((line, index) => ({
229
- line_number: startLine + index,
230
- line_content: line
231
- }));
232
-
233
- // file_content에 라인 번호 추가
234
- const contentWithLineNumbers = selectedLines
235
- .map((line, index) => `${startLine + index}| ${line}`)
236
- .join('\n');
237
-
238
238
  debugLog('========== read_file_range SUCCESS END ==========');
239
239
 
240
240
  return {
@@ -246,7 +246,8 @@ export async function read_file_range({ filePath, startLine, endLine }) {
246
246
  start_line: startLine,
247
247
  end_line: Math.min(endLine, totalLines)
248
248
  },
249
- file_content: contentWithLineNumbers
249
+ file_content: selectedLines.join('\n'),
250
+ md5_hash: md5Hash
250
251
  };
251
252
  } catch (error) {
252
253
  debugLog(`========== read_file_range EXCEPTION ==========`);
@@ -299,7 +300,7 @@ export const readFileRangeSchema = {
299
300
  },
300
301
  "format_tool_result": (result) => {
301
302
  if (result.operation_successful) {
302
- const lineCount = result.file_lines?.length || 0;
303
+ const lineCount = result.file_content ? result.file_content.split('\n').length : 0;
303
304
  return {
304
305
  type: 'formatted',
305
306
  parts: [
@@ -27,6 +27,15 @@ export async function performExit(options = {}) {
27
27
  await uiEvents.waitForRender();
28
28
  }
29
29
 
30
+ // UI 인스턴스 정리 전에 터미널 상태 복구
31
+ // 이렇게 하면 React cleanup이 실행되지 않아도 터미널 상태가 복구됨
32
+ if (process.stdout && process.stdout.write) {
33
+ // 커서 표시 복구 (App.js:447과 동일)
34
+ process.stdout.write('\x1B[?25h');
35
+ // Line wrapping 복구 (index.js:54와 동일)
36
+ process.stdout.write('\x1b[?7h');
37
+ }
38
+
30
39
  // UI 인스턴스 정리
31
40
  if (uiInstance) {
32
41
  uiInstance.unmount();
@@ -22,16 +22,20 @@ export function clampOutput(text) {
22
22
 
23
23
  /**
24
24
  * 파일 읽기 결과에 줄 번호 추가
25
- * @param {Object} result - 파일 읽기 실행 결과
25
+ * @param {string} content - 파일 내용 (문자열)
26
+ * @param {number} startLine - 시작 줄 번호 (기본값: 1)
26
27
  * @returns {string} 줄 번호가 포함된 파일 내용
27
28
  */
28
- export function formatReadFileStdout(result) {
29
- let stdout = '';
30
- if (result.file_lines) {
31
- result.file_lines.map((line, line_number) => {
32
- stdout += `${line_number + 1}|${line}\n`;
33
- });
29
+ export function formatReadFileStdout(content, startLine = 1) {
30
+ if (!content || typeof content !== 'string') {
31
+ return '';
34
32
  }
33
+
34
+ const lines = content.split('\n');
35
+ let stdout = '';
36
+ lines.forEach((line, index) => {
37
+ stdout += `${startLine + index}|${line}\n`;
38
+ });
35
39
  return stdout;
36
40
  }
37
41
 
@@ -42,10 +46,5 @@ export function formatReadFileStdout(result) {
42
46
  * @returns {string} 포맷된 출력
43
47
  */
44
48
  export function formatToolStdout(operation, result) {
45
- switch (operation) {
46
- case 'read_file':
47
- return formatReadFileStdout(result);
48
- default:
49
- return JSON.stringify(result, null, 2);
50
- }
49
+ return JSON.stringify(result, null, 2);
51
50
  }