aiexecode 1.0.111 → 1.0.112

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.

@@ -2,10 +2,10 @@ import dotenv from "dotenv";
2
2
  // 이 파일은 계획을 실제 행동으로 옮기기 위해 어떤 도구를 호출할지 결정합니다.
3
3
  import path from 'path';
4
4
  import { truncateWithOmit } from "../util/text_formatter.js";
5
- import { createSystemMessage, createTodoReminder, createSystemReminder } from "../util/prompt_loader.js";
5
+ import { createSystemMessage, createTodoReminder, createSystemReminder, createTrimmedFileReminder } from "../util/prompt_loader.js";
6
6
  import { request, shouldRetryWithTrim, getModelForProvider } from "../system/ai_request.js";
7
7
  import { supportsCaching } from "../config/ai_models.js";
8
- import { cleanupOrphanOutputs, trimConversation } from "../system/conversation_trimmer.js";
8
+ import { cleanupOrphanOutputs, trimConversation, getTrimmedFileReads, clearTrimmedFileReads } from "../system/conversation_trimmer.js";
9
9
  import { runPythonCodeSchema, bashSchema } from "../system/code_executer.js";
10
10
  import { readFileSchema, readFileRangeSchema } from "../tools/file_reader.js";
11
11
  import { writeFileSchema, editFileRangeSchema, editFileReplaceSchema } from "../tools/code_editor.js";
@@ -99,6 +99,20 @@ async function ensureConversationInitialized() {
99
99
  debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos as separate block (uncached)`);
100
100
  }
101
101
 
102
+ // Trim된 파일 읽기 알림 추가 (파일 경로 알림 기능)
103
+ const trimmedFiles = getTrimmedFileReads();
104
+ if (trimmedFiles.length > 0) {
105
+ const trimmedFileReminder = createTrimmedFileReminder(trimmedFiles);
106
+ if (trimmedFileReminder) {
107
+ contentBlocks.push({
108
+ type: "input_text",
109
+ text: trimmedFileReminder
110
+ // cache_control 없음 - 동적 데이터이므로
111
+ });
112
+ debugLog(`[ensureConversationInitialized] Added trimmed file reminder for ${trimmedFiles.length} files (uncached)`);
113
+ }
114
+ }
115
+
102
116
  systemMessageEntry = {
103
117
  role: "system",
104
118
  content: contentBlocks
@@ -133,6 +147,16 @@ async function ensureConversationInitialized() {
133
147
  debugLog(`[ensureConversationInitialized] Added ${currentTodos.length} todos to system message`);
134
148
  }
135
149
 
150
+ // Trim된 파일 읽기 알림 추가 (파일 경로 알림 기능)
151
+ const trimmedFiles = getTrimmedFileReads();
152
+ if (trimmedFiles.length > 0) {
153
+ const trimmedFileReminder = createTrimmedFileReminder(trimmedFiles);
154
+ if (trimmedFileReminder) {
155
+ systemMessageText += '\n\n' + trimmedFileReminder;
156
+ debugLog(`[ensureConversationInitialized] Added trimmed file reminder for ${trimmedFiles.length} files`);
157
+ }
158
+ }
159
+
136
160
  systemMessageEntry = {
137
161
  role: "system",
138
162
  content: [
@@ -178,6 +202,7 @@ function appendResponseToConversation(response) {
178
202
  export function resetOrchestratorConversation() {
179
203
  orchestratorConversation.length = 0;
180
204
  orchestratorRequestOptions = null;
205
+ clearTrimmedFileReads(); // 세션 초기화 시 trim된 파일 목록도 초기화
181
206
  }
182
207
 
183
208
  export function getOrchestratorConversation() {
@@ -581,14 +581,42 @@ export const runPythonCodeSchema = {
581
581
 
582
582
  export const bashSchema = {
583
583
  name: "bash",
584
- description: "Executes bash shell commands. Use -y or -f flags to avoid user prompts. Chain commands with && for sequential execution. This tool handles ALL file system operations (create/delete/move/copy files and directories, check file existence, list directory contents), system operations, git commands, package management, and running CLI tools.",
584
+ description: `Executes bash shell commands with optional timeout.
585
+
586
+ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file reading/searching operations - use specialized tools instead.
587
+
588
+ Output Limits:
589
+ - stdout is truncated to 8000 characters
590
+ - stderr is truncated to 4000 characters
591
+ - Commands timing out after 20 minutes will be terminated
592
+ - If output exceeds limits, only the first 70% and last 30% are shown with an omission notice
593
+
594
+ Avoid These Commands (Use Specialized Tools Instead):
595
+ - File reading: Use read_file or read_file_range instead of cat, head, tail
596
+ - Content search: Use ripgrep instead of grep or rg
597
+ - File search: Use glob_search instead of find
598
+ - File editing: Use edit_file_replace or write_file instead of sed, awk, echo >
599
+
600
+ When Output May Be Large:
601
+ - Limit output with: command | head -50
602
+ - Save to file: command > output.log 2>&1
603
+ - Filter results: command | grep "pattern"
604
+ - Use quiet/minimal flags when available
605
+
606
+ Best Practices:
607
+ - Use auto-confirm flags: -y, -f, --yes, --quiet
608
+ - Chain commands with && for sequential execution
609
+ - Avoid interactive commands that require user input
610
+ - For calculations, use non-interactive bc or Python (never mental arithmetic)
611
+
612
+ This tool handles: file system operations (mkdir, rm, mv, cp, chmod), git commands, package management (npm, pip), and CLI tools.`,
585
613
  parameters: {
586
614
  type: "object",
587
615
  required: ["script"],
588
616
  properties: {
589
617
  script: {
590
618
  type: "string",
591
- description: "Bash script to execute. Will be saved to a temporary file and run with bash. Use standard bash commands like: mkdir, rm, mv, cp, ls, test, find, chmod, chown, etc."
619
+ description: "Bash script to execute. Will be saved to a temporary file and run with bash. Keep scripts concise and output-limited to avoid truncation."
592
620
  }
593
621
  },
594
622
  additionalProperties: false
@@ -6,9 +6,100 @@
6
6
  */
7
7
 
8
8
  import { createDebugLogger } from "../util/debug_log.js";
9
+ import { clearFileContentHash } from "./file_integrity.js";
9
10
 
10
11
  const debugLog = createDebugLogger('conversation_trimmer.log', 'conversation_trimmer');
11
12
 
13
+ /**
14
+ * Trim 과정에서 삭제된 파일 읽기 정보를 추적합니다.
15
+ * Claude Code 스타일의 "파일 경로 알림" 기능을 위해 사용됩니다.
16
+ *
17
+ * 예: "Note: /path/to/file was read before trimming, but contents are too large to include."
18
+ */
19
+ const trimmedFileReads = new Set();
20
+
21
+ /**
22
+ * trimmedFileReads의 최대 개수 제한
23
+ * 너무 많은 파일이 추적되면 system-reminder가 길어져 토큰 낭비 발생
24
+ */
25
+ const MAX_TRIMMED_FILE_READS = 50;
26
+
27
+ /**
28
+ * Trim된 파일 읽기 목록을 반환합니다.
29
+ * @returns {Array<string>} 삭제된 파일 경로 배열
30
+ */
31
+ export function getTrimmedFileReads() {
32
+ return Array.from(trimmedFileReads);
33
+ }
34
+
35
+ /**
36
+ * Trim된 파일 읽기 목록을 초기화합니다.
37
+ */
38
+ export function clearTrimmedFileReads() {
39
+ trimmedFileReads.clear();
40
+ debugLog(`[clearTrimmedFileReads] Cleared trimmed file reads`);
41
+ }
42
+
43
+ /**
44
+ * Trim된 파일 읽기 목록을 설정합니다 (세션 복원용).
45
+ * 최대 개수 제한을 적용합니다.
46
+ * @param {Array<string>} paths - 복원할 파일 경로 배열
47
+ */
48
+ export function setTrimmedFileReads(paths) {
49
+ trimmedFileReads.clear();
50
+ if (Array.isArray(paths)) {
51
+ // 최대 개수만큼만 추가 (최근 것 우선)
52
+ const pathsToAdd = paths.slice(-MAX_TRIMMED_FILE_READS);
53
+ pathsToAdd.forEach(path => trimmedFileReads.add(path));
54
+ debugLog(`[setTrimmedFileReads] Restored ${trimmedFileReads.size} trimmed file reads`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 파일이 다시 읽혔을 때 trimmedFileReads에서 제거합니다.
60
+ * 파일을 다시 읽으면 대화에 내용이 있으므로 알림이 필요 없습니다.
61
+ * @param {string} filePath - 다시 읽은 파일 경로
62
+ */
63
+ export function markFileAsReRead(filePath) {
64
+ if (trimmedFileReads.has(filePath)) {
65
+ trimmedFileReads.delete(filePath);
66
+ debugLog(`[markFileAsReRead] Removed from trimmed file reads: ${filePath}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 대화 항목에서 파일 읽기 정보를 추출합니다.
72
+ * @param {Object} entry - 대화 항목
73
+ * @returns {Array<string>} 추출된 파일 경로 배열
74
+ */
75
+ function extractFilePathsFromEntry(entry) {
76
+ const filePaths = [];
77
+
78
+ if (!entry) return filePaths;
79
+
80
+ try {
81
+ // function_call_output에서 파일 읽기 결과 추출
82
+ if (entry.type === 'function_call_output' && entry.output) {
83
+ const output = typeof entry.output === 'string' ? JSON.parse(entry.output) : entry.output;
84
+
85
+ // read_file, read_file_range 도구의 결과인지 확인
86
+ if (output.tool === 'read_file' || output.tool === 'read_file_range') {
87
+ const result = output.original_result || output;
88
+ // 성공적으로 읽은 파일만 추적 (실패한 파일은 알림 불필요)
89
+ if (result.target_file_path && result.operation_successful === true) {
90
+ filePaths.push(result.target_file_path);
91
+ debugLog(`[extractFilePathsFromEntry] Found file path: ${result.target_file_path}`);
92
+ }
93
+ }
94
+ }
95
+ } catch (e) {
96
+ // JSON 파싱 실패 등은 무시
97
+ debugLog(`[extractFilePathsFromEntry] Failed to parse entry: ${e.message}`);
98
+ }
99
+
100
+ return filePaths;
101
+ }
102
+
12
103
  /**
13
104
  * 대화에서 특정 call_id와 관련된 모든 항목을 제거합니다.
14
105
  *
@@ -103,6 +194,9 @@ export function cleanupOrphanOutputs(conversation) {
103
194
  * 컨텍스트 윈도우 초과 시 대화를 줄입니다.
104
195
  * 시스템 프롬프트(인덱스 0)를 제외한 가장 오래된 항목(인덱스 1)을 제거합니다.
105
196
  *
197
+ * 삭제되는 항목에서 파일 읽기 정보를 추출하여 trimmedFileReads에 저장합니다.
198
+ * 이 정보는 나중에 system-reminder로 에이전트에게 알려줄 때 사용됩니다.
199
+ *
106
200
  * @param {Array} conversation - 대화 배열
107
201
  * @returns {boolean} 제거 성공 여부
108
202
  */
@@ -119,15 +213,53 @@ export function trimConversation(conversation) {
119
213
 
120
214
  debugLog(`[trimConversation] Target entry at index 1: type=${targetEntry?.type}, call_id=${targetCallId}`);
121
215
 
216
+ // 삭제 전에 파일 경로 추출
217
+ const filePaths = extractFilePathsFromEntry(targetEntry);
218
+ filePaths.forEach(path => {
219
+ // 최대 개수 초과 시 가장 오래된 항목 제거
220
+ if (trimmedFileReads.size >= MAX_TRIMMED_FILE_READS) {
221
+ const oldest = trimmedFileReads.values().next().value;
222
+ trimmedFileReads.delete(oldest);
223
+ debugLog(`[trimConversation] Removed oldest from trimmed file reads: ${oldest}`);
224
+ }
225
+ trimmedFileReads.add(path);
226
+ debugLog(`[trimConversation] Added to trimmed file reads: ${path}`);
227
+
228
+ // file_integrity에서 해시도 삭제 (에이전트가 파일 내용을 모르므로 편집 불가하게)
229
+ const hashCleared = clearFileContentHash(path);
230
+ debugLog(`[trimConversation] Cleared content hash for ${path}: ${hashCleared}`);
231
+ });
232
+
122
233
  // 먼저 인덱스 1 항목 제거
123
234
  conversation.splice(1, 1);
124
235
  debugLog(`[trimConversation] Removed entry at index 1`);
125
236
 
126
237
  // call_id가 있으면 같은 call_id를 가진 다른 항목들도 제거
127
238
  if (targetCallId) {
239
+ // 삭제 전에 관련 항목들에서 파일 경로 추출
240
+ for (const entry of conversation) {
241
+ if (entry && entry.call_id === targetCallId) {
242
+ const paths = extractFilePathsFromEntry(entry);
243
+ paths.forEach(path => {
244
+ // 최대 개수 초과 시 가장 오래된 항목 제거
245
+ if (trimmedFileReads.size >= MAX_TRIMMED_FILE_READS) {
246
+ const oldest = trimmedFileReads.values().next().value;
247
+ trimmedFileReads.delete(oldest);
248
+ debugLog(`[trimConversation] Removed oldest from trimmed file reads: ${oldest}`);
249
+ }
250
+ trimmedFileReads.add(path);
251
+ debugLog(`[trimConversation] Added related file to trimmed reads: ${path}`);
252
+
253
+ // file_integrity에서 해시도 삭제 (에이전트가 파일 내용을 모르므로 편집 불가하게)
254
+ const hashCleared = clearFileContentHash(path);
255
+ debugLog(`[trimConversation] Cleared content hash for ${path}: ${hashCleared}`);
256
+ });
257
+ }
258
+ }
128
259
  removeEntriesWithCallId(conversation, targetCallId);
129
260
  }
130
261
 
131
262
  debugLog(`[trimConversation] Trim completed. New conversation length: ${conversation.length}`);
263
+ debugLog(`[trimConversation] Total trimmed file reads: ${trimmedFileReads.size}`);
132
264
  return true;
133
265
  }
@@ -112,6 +112,38 @@ class FileIntegrityTracker {
112
112
  return sessionFiles.get(filePath) || null;
113
113
  }
114
114
 
115
+ /**
116
+ * 파일 콘텐츠 해시를 삭제합니다
117
+ * 대화가 trim되어 에이전트가 파일 내용을 알 수 없게 되었을 때 호출됩니다.
118
+ * 이렇게 하면 에이전트가 파일을 다시 읽기 전까지 편집할 수 없습니다.
119
+ * @param {string} sessionID - 세션 ID
120
+ * @param {string} filePath - 파일 경로
121
+ * @returns {boolean} 삭제 성공 여부
122
+ */
123
+ clearContentHash(sessionID, filePath) {
124
+ debugLog(`========== clearContentHash START ==========`);
125
+ debugLog(`sessionID: ${sessionID}`);
126
+ debugLog(`filePath: ${filePath}`);
127
+
128
+ const sessionFiles = this.contentHashes.get(sessionID);
129
+ if (!sessionFiles) {
130
+ debugLog(`No session map found for session: ${sessionID}`);
131
+ debugLog(`========== clearContentHash END (NO SESSION) ==========`);
132
+ return false;
133
+ }
134
+
135
+ const existed = sessionFiles.has(filePath);
136
+ if (existed) {
137
+ sessionFiles.delete(filePath);
138
+ debugLog(`Hash deleted for file: ${filePath}`);
139
+ } else {
140
+ debugLog(`No hash found for file: ${filePath}`);
141
+ }
142
+
143
+ debugLog(`========== clearContentHash END ==========`);
144
+ return existed;
145
+ }
146
+
115
147
  /**
116
148
  * 파일 편집 전 무결성을 검증합니다
117
149
  * @param {string} sessionID - 세션 ID
@@ -347,6 +379,31 @@ export async function assertFileIntegrity(filePath) {
347
379
  debugLog('========== assertFileIntegrity (export wrapper) END ==========');
348
380
  }
349
381
 
382
+ /**
383
+ * 파일 콘텐츠 해시를 삭제합니다 (현재 세션 기준)
384
+ * 대화가 trim되어 에이전트가 파일 내용을 알 수 없게 되었을 때 호출됩니다.
385
+ * @param {string} filePath - 파일 경로
386
+ * @returns {boolean} 삭제 성공 여부
387
+ */
388
+ export function clearFileContentHash(filePath) {
389
+ debugLog(`========== clearFileContentHash (export wrapper) START ==========`);
390
+ debugLog(`filePath: ${filePath}`);
391
+
392
+ const sessionID = fileIntegrityTracker.getCurrentSession();
393
+ debugLog(`Current session ID: ${sessionID || 'NULL'}`);
394
+
395
+ if (!sessionID) {
396
+ debugLog(`ERROR: No current session set, cannot clear hash`);
397
+ debugLog('========== clearFileContentHash (export wrapper) END ==========');
398
+ return false;
399
+ }
400
+
401
+ const result = fileIntegrityTracker.clearContentHash(sessionID, filePath);
402
+ debugLog(`Hash clear result: ${result}`);
403
+ debugLog('========== clearFileContentHash (export wrapper) END ==========');
404
+ return result;
405
+ }
406
+
350
407
  /**
351
408
  * 파일 스냅샷을 저장합니다 (현재 세션 기준)
352
409
  * @param {string} filePath - 파일 경로
@@ -12,7 +12,7 @@ import { TODO_WRITE_FUNCTIONS } from '../tools/todo_write.js';
12
12
  import { SKILL_FUNCTIONS } from '../tools/skill_tool.js';
13
13
  import { clampOutput, formatToolStdout } from "../util/output_formatter.js";
14
14
  import { buildToolHistoryEntry } from "../util/rag_helper.js";
15
- import { createSessionData, getLastConversationState, loadPreviousSessions, saveSessionToHistory, saveTodosToSession, restoreTodosFromSession, updateCurrentTodos, getCurrentTodos } from "./session_memory.js";
15
+ import { createSessionData, getLastConversationState, loadPreviousSessions, saveSessionToHistory, saveTodosToSession, restoreTodosFromSession, saveTrimmedFileReadsToSession, restoreTrimmedFileReadsFromSession, updateCurrentTodos, getCurrentTodos } from "./session_memory.js";
16
16
  import { uiEvents } from "./ui_events.js";
17
17
  import { logSystem, logError, logAssistantMessage, logToolCall, logToolResult, logCodeExecution, logCodeResult, logIteration, logMissionComplete, logConversationRestored } from "./output_helper.js";
18
18
  import { requiresApproval, requestApproval } from "./tool_approval.js";
@@ -887,6 +887,11 @@ export async function runSession(options) {
887
887
  restoreTodosFromSession({ currentTodos: conversationState.currentTodos });
888
888
  debugLog(`[runSession] Restored ${conversationState.currentTodos.length} todos from previous session`);
889
889
  }
890
+ // TrimmedFileReads 복원
891
+ if (conversationState.trimmedFileReads) {
892
+ restoreTrimmedFileReadsFromSession({ trimmedFileReads: conversationState.trimmedFileReads });
893
+ debugLog(`[runSession] Restored ${conversationState.trimmedFileReads.length} trimmed file reads from previous session`);
894
+ }
890
895
  // logSuccess('✓ Conversation state restored');
891
896
  } else {
892
897
  // logSystem('ℹ Starting with fresh conversation state');
@@ -1054,6 +1059,9 @@ export async function runSession(options) {
1054
1059
  // Todos를 세션에 저장
1055
1060
  saveTodosToSession(currentSessionData);
1056
1061
 
1062
+ // TrimmedFileReads를 세션에 저장
1063
+ saveTrimmedFileReadsToSession(currentSessionData);
1064
+
1057
1065
  debugLog(`[ITERATION ${iteration_count}] Saving session to history after completion_judge (mission_solved=${mission_solved})`);
1058
1066
  await saveSessionToHistory(currentSessionData).catch(err => {
1059
1067
  debugLog(`[ITERATION ${iteration_count}] Failed to save session after completion_judge: ${err.message}`);
@@ -1082,10 +1090,14 @@ export async function runSession(options) {
1082
1090
  currentSessionData.orchestratorConversation = getOrchestratorConversation();
1083
1091
  currentSessionData.orchestratorRequestOptions = null;
1084
1092
 
1085
- debugLog(`[ITERATION ${iteration_count}] Session interrupted - NOT saving session to history (disabled)`);
1086
- // await saveSessionToHistory(currentSessionData).catch(err => {
1087
- // debugLog(`[ITERATION ${iteration_count}] Failed to save session: ${err.message}`);
1088
- // });
1093
+ // 인터럽트 시에도 Todos와 TrimmedFileReads 저장 (다음 세션에서 복원 가능하도록)
1094
+ saveTodosToSession(currentSessionData);
1095
+ saveTrimmedFileReadsToSession(currentSessionData);
1096
+
1097
+ debugLog(`[ITERATION ${iteration_count}] Session interrupted - saving trimmedFileReads and todos`);
1098
+ await saveSessionToHistory(currentSessionData).catch(err => {
1099
+ debugLog(`[ITERATION ${iteration_count}] Failed to save session on interrupt: ${err.message}`);
1100
+ });
1089
1101
  break;
1090
1102
  }
1091
1103
 
@@ -1119,6 +1131,21 @@ export async function runSession(options) {
1119
1131
  : '✅ No function calls detected - mission complete';
1120
1132
  uiEvents.addSystemMessage(message);
1121
1133
  debugLog(`[ITERATION ${iteration_count}] ${message}`);
1134
+
1135
+ // completion_judge 없이 종료되는 경우에도 Todos와 TrimmedFileReads 저장
1136
+ currentSessionData.completed_at = new Date().toISOString();
1137
+ currentSessionData.mission_solved = true;
1138
+ currentSessionData.iteration_count = iteration_count;
1139
+ currentSessionData.toolUsageHistory = toolUsageHistory;
1140
+ currentSessionData.orchestratorConversation = getOrchestratorConversation();
1141
+ currentSessionData.orchestratorRequestOptions = null;
1142
+ saveTodosToSession(currentSessionData);
1143
+ saveTrimmedFileReadsToSession(currentSessionData);
1144
+ debugLog(`[ITERATION ${iteration_count}] Saving session on no-function-call exit`);
1145
+ await saveSessionToHistory(currentSessionData).catch(err => {
1146
+ debugLog(`[ITERATION ${iteration_count}] Failed to save session: ${err.message}`);
1147
+ });
1148
+
1122
1149
  debugLog('========================================');
1123
1150
  debugLog(`========== ITERATION ${iteration_count} END ==========`);
1124
1151
  debugLog('========================================');
@@ -1158,12 +1185,17 @@ export async function runSession(options) {
1158
1185
  currentSessionData.orchestratorConversation = result.orchestratorConversation;
1159
1186
  currentSessionData.orchestratorRequestOptions = null; // 필요시 orchestrator에서 가져올 수 있음
1160
1187
 
1161
- debugLog(`[runSession] NOT saving final session to history (disabled - will only save after completion_judge) - sessionID: ${currentSessionData.sessionID}`);
1188
+ // 루프 정상 종료 시에도 Todos와 TrimmedFileReads 저장
1189
+ saveTodosToSession(currentSessionData);
1190
+ saveTrimmedFileReadsToSession(currentSessionData);
1191
+
1192
+ debugLog(`[runSession] Saving final session to history - sessionID: ${currentSessionData.sessionID}`);
1162
1193
  debugLog(`[runSession] Session data size: ${JSON.stringify(currentSessionData).length} bytes`);
1163
1194
  debugLog(`[runSession] Final state - mission_solved: ${currentSessionData.mission_solved}, iteration_count: ${currentSessionData.iteration_count}`);
1164
1195
  debugLog(`[runSession] Tool usage history entries: ${currentSessionData.toolUsageHistory.length}`);
1165
- // 최종 세션 히스토리에 저장 (중복 저장이지만 최신 상태 보장) - DISABLED
1166
- // await saveSessionToHistory(currentSessionData);
1196
+ await saveSessionToHistory(currentSessionData).catch(err => {
1197
+ debugLog(`[runSession] Failed to save final session: ${err.message}`);
1198
+ });
1167
1199
 
1168
1200
  debugLog('========================================');
1169
1201
  debugLog('========== runSession END ==========');
@@ -1198,6 +1230,18 @@ export async function runSession(options) {
1198
1230
 
1199
1231
  uiEvents.addErrorMessage(consolidatedErrorMessage);
1200
1232
 
1233
+ // 에러 발생 시에도 Todos와 TrimmedFileReads 저장 시도
1234
+ if (currentSessionData) {
1235
+ currentSessionData.completed_at = new Date().toISOString();
1236
+ currentSessionData.mission_solved = false;
1237
+ saveTodosToSession(currentSessionData);
1238
+ saveTrimmedFileReadsToSession(currentSessionData);
1239
+ debugLog(`[runSession] Saving session on error`);
1240
+ await saveSessionToHistory(currentSessionData).catch(err => {
1241
+ debugLog(`[runSession] Failed to save session on error: ${err.message}`);
1242
+ });
1243
+ }
1244
+
1201
1245
  // 에러를 throw하지 않고 정상적으로 종료
1202
1246
  return null;
1203
1247
  } finally {
@@ -4,6 +4,7 @@ import { join, resolve } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { formatToolCall, formatToolResult, getToolDisplayName, getToolDisplayConfig } from './tool_registry.js';
6
6
  import { createDebugLogger } from '../util/debug_log.js';
7
+ import { getTrimmedFileReads, setTrimmedFileReads } from './conversation_trimmer.js';
7
8
 
8
9
  const MAX_HISTORY_SESSIONS = 1; // 최대 보관 세션 수
9
10
 
@@ -11,7 +12,7 @@ const debugLog = createDebugLogger('session_memory.log', 'session_memory');
11
12
 
12
13
  // 현재 작업 디렉토리 기준 세션 디렉토리 경로 생성
13
14
  function getSessionDir(sessionID) {
14
- return join(process.cwd(), '.aiexe', sessionID);
15
+ return join(process.cwd(), '.aiexe', 'sessions', sessionID);
15
16
  }
16
17
 
17
18
  // 세션 히스토리 파일 경로 생성
@@ -150,7 +151,8 @@ export function getLastConversationState(sessions) {
150
151
  return {
151
152
  orchestratorConversation: lastSession.orchestratorConversation || [],
152
153
  orchestratorRequestOptions: lastSession.orchestratorRequestOptions || null,
153
- currentTodos: lastSession.currentTodos || []
154
+ currentTodos: lastSession.currentTodos || [],
155
+ trimmedFileReads: lastSession.trimmedFileReads || []
154
156
  };
155
157
  }
156
158
 
@@ -450,3 +452,29 @@ export function restoreTodosFromSession(sessionData) {
450
452
  currentSessionTodos = [];
451
453
  }
452
454
  }
455
+
456
+ /**
457
+ * 세션 데이터에 trimmedFileReads를 저장
458
+ * @param {Object} sessionData - 세션 데이터 객체
459
+ */
460
+ export function saveTrimmedFileReadsToSession(sessionData) {
461
+ debugLog('========== saveTrimmedFileReadsToSession ==========');
462
+ const trimmedFiles = getTrimmedFileReads();
463
+ debugLog(`Saving ${trimmedFiles.length} trimmed file reads to session`);
464
+ sessionData.trimmedFileReads = trimmedFiles;
465
+ }
466
+
467
+ /**
468
+ * 세션 데이터에서 trimmedFileReads를 복원
469
+ * @param {Object} sessionData - 세션 데이터 객체
470
+ */
471
+ export function restoreTrimmedFileReadsFromSession(sessionData) {
472
+ debugLog('========== restoreTrimmedFileReadsFromSession ==========');
473
+ if (sessionData && Array.isArray(sessionData.trimmedFileReads)) {
474
+ debugLog(`Restoring ${sessionData.trimmedFileReads.length} trimmed file reads from session`);
475
+ setTrimmedFileReads(sessionData.trimmedFileReads);
476
+ } else {
477
+ debugLog('No trimmed file reads to restore');
478
+ setTrimmedFileReads([]);
479
+ }
480
+ }