gekto 0.0.7 → 0.0.9
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.
- package/dist/agents/HeadlessAgent.js +101 -10
- package/dist/agents/agentPool.js +190 -7
- package/dist/agents/agentWebSocket.js +777 -79
- package/dist/agents/bashSafetyRules.js +30 -0
- package/dist/agents/gektoPersistent.js +190 -31
- package/dist/agents/gektoSimple.js +7 -2
- package/dist/agents/gektoTools.js +439 -105
- package/dist/agents/types.js +2 -0
- package/dist/entityStore.js +499 -0
- package/dist/proxy.js +91 -55
- package/dist/state.js +251 -0
- package/dist/widget/gekto-widget.iife.js +717 -204
- package/package.json +1 -1
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
3
|
import { CLAUDE_PATH } from '../claudePath.js';
|
|
3
4
|
export class HeadlessAgent {
|
|
4
5
|
sessionId = null;
|
|
5
6
|
config;
|
|
6
7
|
currentProc = null;
|
|
8
|
+
pendingFileChanges = new Map();
|
|
7
9
|
constructor(config = {}) {
|
|
8
10
|
this.config = config;
|
|
9
11
|
}
|
|
12
|
+
// Read file content safely, returns null if file doesn't exist
|
|
13
|
+
readFileSafe(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
// Resolve path relative to working directory
|
|
16
|
+
const fullPath = filePath.startsWith('/')
|
|
17
|
+
? filePath
|
|
18
|
+
: `${this.config.workingDir || process.cwd()}/${filePath}`;
|
|
19
|
+
if (!existsSync(fullPath))
|
|
20
|
+
return null;
|
|
21
|
+
return readFileSync(fullPath, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
kill() {
|
|
11
28
|
if (this.currentProc && !this.currentProc.killed) {
|
|
12
29
|
this.currentProc.kill('SIGTERM');
|
|
@@ -18,9 +35,15 @@ export class HeadlessAgent {
|
|
|
18
35
|
isRunning() {
|
|
19
36
|
return this.currentProc !== null && !this.currentProc.killed;
|
|
20
37
|
}
|
|
21
|
-
async send(message, callbacks) {
|
|
38
|
+
async send(message, callbacks, imagePaths) {
|
|
39
|
+
// Append image file paths to the message so Claude can read them
|
|
40
|
+
let finalMessage = message;
|
|
41
|
+
if (imagePaths && imagePaths.length > 0) {
|
|
42
|
+
const imageRefs = imagePaths.map(p => ` - ${p}`).join('\n');
|
|
43
|
+
finalMessage += `\n\n[The user attached ${imagePaths.length} image(s). Use the Read tool to view them:\n${imageRefs}]`;
|
|
44
|
+
}
|
|
22
45
|
const args = [
|
|
23
|
-
'-p',
|
|
46
|
+
'-p', finalMessage,
|
|
24
47
|
'--output-format', 'stream-json',
|
|
25
48
|
'--verbose',
|
|
26
49
|
'--model', 'claude-opus-4-5-20251101',
|
|
@@ -54,6 +77,8 @@ export class HeadlessAgent {
|
|
|
54
77
|
let buffer = '';
|
|
55
78
|
let lastResult = null;
|
|
56
79
|
let currentTool = null;
|
|
80
|
+
const toolUseIdToName = new Map();
|
|
81
|
+
const streamState = { receivedDeltas: false };
|
|
57
82
|
proc.stdout.on('data', (data) => {
|
|
58
83
|
buffer += data.toString();
|
|
59
84
|
const lines = buffer.split('\n');
|
|
@@ -63,7 +88,7 @@ export class HeadlessAgent {
|
|
|
63
88
|
continue;
|
|
64
89
|
try {
|
|
65
90
|
const event = JSON.parse(line);
|
|
66
|
-
this.processStreamEvent(event, callbacks, (tool) => {
|
|
91
|
+
this.processStreamEvent(event, callbacks, toolUseIdToName, streamState, (tool) => {
|
|
67
92
|
currentTool = tool;
|
|
68
93
|
}, () => {
|
|
69
94
|
currentTool = null;
|
|
@@ -108,7 +133,7 @@ export class HeadlessAgent {
|
|
|
108
133
|
proc.on('error', reject);
|
|
109
134
|
});
|
|
110
135
|
}
|
|
111
|
-
processStreamEvent(event, callbacks, setCurrentTool, clearCurrentTool) {
|
|
136
|
+
processStreamEvent(event, callbacks, toolUseIdToName, streamState, setCurrentTool, clearCurrentTool) {
|
|
112
137
|
if (event.type === 'assistant' && event.message) {
|
|
113
138
|
const message = event.message;
|
|
114
139
|
if (message.content) {
|
|
@@ -116,30 +141,96 @@ export class HeadlessAgent {
|
|
|
116
141
|
if (block.type === 'tool_use' && block.name) {
|
|
117
142
|
setCurrentTool?.(block.name);
|
|
118
143
|
callbacks?.onToolStart?.(block.name, block.input);
|
|
144
|
+
// Track tool name by ID for tool_result matching
|
|
145
|
+
if (block.id) {
|
|
146
|
+
toolUseIdToName?.set(block.id, block.name);
|
|
147
|
+
}
|
|
148
|
+
// Track file changes for Write/Edit tools
|
|
149
|
+
if ((block.name === 'Write' || block.name === 'Edit') && block.id && block.input?.file_path) {
|
|
150
|
+
const filePath = String(block.input.file_path);
|
|
151
|
+
const before = this.readFileSafe(filePath);
|
|
152
|
+
this.pendingFileChanges.set(block.id, {
|
|
153
|
+
tool: block.name,
|
|
154
|
+
filePath,
|
|
155
|
+
before,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Fallback: emit full text block only if no streaming deltas were received
|
|
160
|
+
if (block.type === 'text' && block.text && !streamState?.receivedDeltas) {
|
|
161
|
+
callbacks?.onText?.(block.text);
|
|
162
|
+
}
|
|
163
|
+
// Thinking block from assistant event (fallback when no thinking_delta)
|
|
164
|
+
if (block.type === 'thinking' && block.thinking && !streamState?.receivedDeltas) {
|
|
165
|
+
callbacks?.onThinking?.(block.thinking);
|
|
119
166
|
}
|
|
120
167
|
}
|
|
121
168
|
}
|
|
122
169
|
}
|
|
170
|
+
// Streaming deltas — character-by-character text and thinking
|
|
171
|
+
if (event.type === 'content_block_delta') {
|
|
172
|
+
const delta = event.delta;
|
|
173
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
174
|
+
if (streamState)
|
|
175
|
+
streamState.receivedDeltas = true;
|
|
176
|
+
callbacks?.onText?.(delta.text);
|
|
177
|
+
}
|
|
178
|
+
else if (delta?.type === 'thinking_delta' && delta.thinking) {
|
|
179
|
+
if (streamState)
|
|
180
|
+
streamState.receivedDeltas = true;
|
|
181
|
+
callbacks?.onThinking?.(delta.thinking);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
123
184
|
if (event.type === 'user' && event.message) {
|
|
124
185
|
const message = event.message;
|
|
125
186
|
if (message.content) {
|
|
126
187
|
for (const block of message.content) {
|
|
127
188
|
if (block.type === 'tool_result') {
|
|
128
189
|
clearCurrentTool?.();
|
|
190
|
+
// Extract tool result content
|
|
191
|
+
if (block.tool_use_id && block.content != null) {
|
|
192
|
+
const toolName = toolUseIdToName?.get(block.tool_use_id) || 'unknown';
|
|
193
|
+
let resultText = '';
|
|
194
|
+
if (typeof block.content === 'string') {
|
|
195
|
+
resultText = block.content;
|
|
196
|
+
}
|
|
197
|
+
else if (Array.isArray(block.content)) {
|
|
198
|
+
// content is array of {type: 'text', text: '...'} blocks
|
|
199
|
+
resultText = block.content
|
|
200
|
+
.filter(c => c.type === 'text' && c.text)
|
|
201
|
+
.map(c => c.text)
|
|
202
|
+
.join('\n');
|
|
203
|
+
}
|
|
204
|
+
if (resultText) {
|
|
205
|
+
callbacks?.onToolResult?.(toolName, resultText, block.tool_use_id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Complete file change tracking
|
|
209
|
+
if (block.tool_use_id && this.pendingFileChanges.has(block.tool_use_id)) {
|
|
210
|
+
const pending = this.pendingFileChanges.get(block.tool_use_id);
|
|
211
|
+
this.pendingFileChanges.delete(block.tool_use_id);
|
|
212
|
+
const after = this.readFileSafe(pending.filePath);
|
|
213
|
+
// Only emit if file actually changed (or was created)
|
|
214
|
+
if (after !== null && after !== pending.before) {
|
|
215
|
+
callbacks?.onFileChange?.({
|
|
216
|
+
tool: pending.tool,
|
|
217
|
+
filePath: pending.filePath,
|
|
218
|
+
before: pending.before,
|
|
219
|
+
after,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
129
223
|
}
|
|
130
224
|
}
|
|
131
225
|
}
|
|
132
226
|
}
|
|
133
|
-
if (event.type === 'content_block_delta') {
|
|
134
|
-
const delta = event.delta;
|
|
135
|
-
if (delta?.type === 'text_delta' && delta.text) {
|
|
136
|
-
callbacks?.onText?.(delta.text);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
227
|
}
|
|
140
228
|
getSessionId() {
|
|
141
229
|
return this.sessionId;
|
|
142
230
|
}
|
|
231
|
+
setSessionId(id) {
|
|
232
|
+
this.sessionId = id;
|
|
233
|
+
}
|
|
143
234
|
resetSession() {
|
|
144
235
|
this.sessionId = null;
|
|
145
236
|
}
|
package/dist/agents/agentPool.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
2
6
|
import { HeadlessAgent } from './HeadlessAgent.js';
|
|
7
|
+
import { getState, mutate, broadcastFileChange, broadcastAgent } from '../state.js';
|
|
3
8
|
// Per-lizard sessions
|
|
4
9
|
const sessions = new Map();
|
|
5
10
|
// Summarize tool input for display
|
|
@@ -28,7 +33,16 @@ Your job is ONLY to:
|
|
|
28
33
|
- Write and edit code using Write and Edit tools
|
|
29
34
|
- Make the requested code changes
|
|
30
35
|
|
|
31
|
-
After making changes, simply report what you did. The user will handle building, testing, and running the code themselves
|
|
36
|
+
After making changes, simply report what you did. The user will handle building, testing, and running the code themselves.
|
|
37
|
+
|
|
38
|
+
STATUS MARKER - At the END of EVERY response, you MUST include exactly one of these markers:
|
|
39
|
+
- [STATUS:DONE] - Use when the task is complete and you have no questions for the user
|
|
40
|
+
- [STATUS:PENDING] - Use when you need user input, confirmation, clarification, or approval to proceed
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
- After completing a code change: "I've updated the function. [STATUS:DONE]"
|
|
44
|
+
- When asking a question: "Which approach would you prefer? [STATUS:PENDING]"
|
|
45
|
+
- After answering a simple question: "The file is located at src/utils.ts [STATUS:DONE]"`;
|
|
32
46
|
// Tools that agents are not allowed to use
|
|
33
47
|
const DISALLOWED_TOOLS = ['Bash', 'Task'];
|
|
34
48
|
function getOrCreateSession(lizardId, ws) {
|
|
@@ -52,6 +66,31 @@ function getOrCreateSession(lizardId, ws) {
|
|
|
52
66
|
}
|
|
53
67
|
return session;
|
|
54
68
|
}
|
|
69
|
+
export function resumeSession(lizardId, sessionId, ws) {
|
|
70
|
+
let session = sessions.get(lizardId);
|
|
71
|
+
if (!session) {
|
|
72
|
+
const agent = new HeadlessAgent({
|
|
73
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
74
|
+
workingDir: getWorkingDir(),
|
|
75
|
+
disallowedTools: DISALLOWED_TOOLS,
|
|
76
|
+
});
|
|
77
|
+
// Restore Claude Code session ID so --resume works
|
|
78
|
+
if (sessionId) {
|
|
79
|
+
agent.setSessionId(sessionId);
|
|
80
|
+
}
|
|
81
|
+
session = {
|
|
82
|
+
agent,
|
|
83
|
+
isProcessing: false,
|
|
84
|
+
queue: [],
|
|
85
|
+
currentWs: ws ?? null,
|
|
86
|
+
};
|
|
87
|
+
sessions.set(lizardId, session);
|
|
88
|
+
}
|
|
89
|
+
else if (ws) {
|
|
90
|
+
session.currentWs = ws;
|
|
91
|
+
}
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
55
94
|
export function isProcessing(lizardId) {
|
|
56
95
|
const session = sessions.get(lizardId);
|
|
57
96
|
return session?.isProcessing ?? false;
|
|
@@ -64,14 +103,51 @@ export function getQueueLength(lizardId) {
|
|
|
64
103
|
function safeSend(session, data) {
|
|
65
104
|
const ws = session.currentWs;
|
|
66
105
|
if (ws && ws.readyState === ws.OPEN) {
|
|
106
|
+
const type = data.type;
|
|
107
|
+
const lizardId = data.lizardId;
|
|
108
|
+
// Log outgoing messages (skip noisy streaming deltas)
|
|
109
|
+
if (type !== 'text' && type !== 'thinking') {
|
|
110
|
+
console.log(`[WS→] ${type}${lizardId ? ` [${lizardId}]` : ''}${data.tool ? ` tool=${data.tool}` : ''}${data.state ? ` state=${data.state}` : ''}`);
|
|
111
|
+
}
|
|
67
112
|
ws.send(JSON.stringify(data));
|
|
68
113
|
}
|
|
69
114
|
}
|
|
70
|
-
|
|
115
|
+
// Save base64 data URL images to temp files and return file paths
|
|
116
|
+
export function saveImagesToTempFiles(images) {
|
|
117
|
+
const paths = [];
|
|
118
|
+
const dir = path.join(tmpdir(), 'gekto-images');
|
|
119
|
+
if (!existsSync(dir)) {
|
|
120
|
+
mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
for (const dataUrl of images) {
|
|
123
|
+
const match = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
124
|
+
if (!match)
|
|
125
|
+
continue;
|
|
126
|
+
const ext = match[1] === 'jpeg' ? 'jpg' : match[1];
|
|
127
|
+
const buffer = Buffer.from(match[2], 'base64');
|
|
128
|
+
const filePath = path.join(dir, `gekto-img-${randomUUID()}.${ext}`);
|
|
129
|
+
writeFileSync(filePath, buffer);
|
|
130
|
+
paths.push(filePath);
|
|
131
|
+
}
|
|
132
|
+
return paths;
|
|
133
|
+
}
|
|
134
|
+
export async function sendMessage(lizardId, message, ws, images) {
|
|
71
135
|
const session = getOrCreateSession(lizardId, ws);
|
|
136
|
+
// Save images to temp files and build the final message
|
|
137
|
+
let finalMessage = message;
|
|
138
|
+
let imagePaths;
|
|
139
|
+
if (images && images.length > 0) {
|
|
140
|
+
imagePaths = saveImagesToTempFiles(images);
|
|
141
|
+
}
|
|
142
|
+
// Accumulators for streaming deltas — reset on each tool start
|
|
143
|
+
let accumulatedText = '';
|
|
144
|
+
let accumulatedThinking = '';
|
|
72
145
|
// Create streaming callbacks that use session's current WebSocket
|
|
73
146
|
const callbacks = {
|
|
74
147
|
onToolStart: (tool, input) => {
|
|
148
|
+
// Reset accumulators when a new tool starts (text block ended)
|
|
149
|
+
accumulatedText = '';
|
|
150
|
+
accumulatedThinking = '';
|
|
75
151
|
safeSend(session, {
|
|
76
152
|
type: 'tool',
|
|
77
153
|
lizardId,
|
|
@@ -89,11 +165,76 @@ export async function sendMessage(lizardId, message, ws) {
|
|
|
89
165
|
tool,
|
|
90
166
|
});
|
|
91
167
|
},
|
|
168
|
+
onText: (text) => {
|
|
169
|
+
accumulatedText += text;
|
|
170
|
+
safeSend(session, {
|
|
171
|
+
type: 'text',
|
|
172
|
+
lizardId,
|
|
173
|
+
text: accumulatedText,
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
onThinking: (text) => {
|
|
177
|
+
accumulatedThinking += text;
|
|
178
|
+
safeSend(session, {
|
|
179
|
+
type: 'thinking',
|
|
180
|
+
lizardId,
|
|
181
|
+
text: accumulatedThinking,
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
onToolResult: (tool, content, toolUseId) => {
|
|
185
|
+
safeSend(session, {
|
|
186
|
+
type: 'tool_result',
|
|
187
|
+
lizardId,
|
|
188
|
+
tool,
|
|
189
|
+
content: content.length > 2000 ? content.substring(0, 2000) + '…' : content,
|
|
190
|
+
toolUseId,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
onFileChange: (change) => {
|
|
194
|
+
// Encode path for use as key
|
|
195
|
+
const encodedPath = change.filePath.replace(/\//g, '--');
|
|
196
|
+
// Enrich change with metadata
|
|
197
|
+
const enrichedChange = {
|
|
198
|
+
...change,
|
|
199
|
+
agentId: lizardId,
|
|
200
|
+
taskId: getState().agents[lizardId]?.taskId,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
// Write to top-level fileChanges collection
|
|
204
|
+
mutate(`fileChanges.${encodedPath}`, enrichedChange);
|
|
205
|
+
broadcastFileChange(encodedPath);
|
|
206
|
+
// Also update agent's fileChangePaths for reference
|
|
207
|
+
const agent = getState().agents[lizardId];
|
|
208
|
+
if (agent) {
|
|
209
|
+
const paths = agent.fileChangePaths ?? [];
|
|
210
|
+
if (!paths.includes(change.filePath)) {
|
|
211
|
+
mutate(`agents.${lizardId}.fileChangePaths`, [...paths, change.filePath]);
|
|
212
|
+
}
|
|
213
|
+
// Keep backward-compat fileChanges on agent
|
|
214
|
+
const existing = agent.fileChanges ?? [];
|
|
215
|
+
const existingIndex = existing.findIndex(fc => fc.filePath === change.filePath);
|
|
216
|
+
let updated;
|
|
217
|
+
if (existingIndex >= 0) {
|
|
218
|
+
updated = [...existing];
|
|
219
|
+
updated[existingIndex] = { ...updated[existingIndex], after: change.after, tool: change.tool };
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
updated = [...existing, enrichedChange];
|
|
223
|
+
}
|
|
224
|
+
mutate(`agents.${lizardId}.fileChanges`, updated);
|
|
225
|
+
broadcastAgent(lizardId);
|
|
226
|
+
}
|
|
227
|
+
safeSend(session, {
|
|
228
|
+
type: 'file_change',
|
|
229
|
+
lizardId,
|
|
230
|
+
change: enrichedChange,
|
|
231
|
+
});
|
|
232
|
+
},
|
|
92
233
|
};
|
|
93
234
|
// If already processing, queue the message
|
|
94
235
|
if (session.isProcessing) {
|
|
95
236
|
return new Promise((resolve, reject) => {
|
|
96
|
-
session.queue.push({ message, ws, callbacks, resolve, reject });
|
|
237
|
+
session.queue.push({ message: finalMessage, ws, callbacks, resolve, reject, imagePaths });
|
|
97
238
|
const position = session.queue.length;
|
|
98
239
|
ws.send(JSON.stringify({
|
|
99
240
|
type: 'queued',
|
|
@@ -103,14 +244,14 @@ export async function sendMessage(lizardId, message, ws) {
|
|
|
103
244
|
});
|
|
104
245
|
}
|
|
105
246
|
// Process immediately
|
|
106
|
-
return processMessage(lizardId, session,
|
|
247
|
+
return processMessage(lizardId, session, finalMessage, ws, callbacks, imagePaths);
|
|
107
248
|
}
|
|
108
249
|
async function processMessage(lizardId, session, message, _ws, // Kept for queue compatibility, but we use session.currentWs
|
|
109
|
-
callbacks) {
|
|
250
|
+
callbacks, imagePaths) {
|
|
110
251
|
session.isProcessing = true;
|
|
111
252
|
safeSend(session, { type: 'state', lizardId, state: 'working' });
|
|
112
253
|
try {
|
|
113
|
-
const response = await session.agent.send(message, callbacks);
|
|
254
|
+
const response = await session.agent.send(message, callbacks, imagePaths);
|
|
114
255
|
safeSend(session, {
|
|
115
256
|
type: 'response',
|
|
116
257
|
lizardId,
|
|
@@ -135,7 +276,7 @@ callbacks) {
|
|
|
135
276
|
// Process next queued message if any
|
|
136
277
|
if (session.queue.length > 0) {
|
|
137
278
|
const next = session.queue.shift();
|
|
138
|
-
processMessage(lizardId, session, next.message, next.ws, next.callbacks)
|
|
279
|
+
processMessage(lizardId, session, next.message, next.ws, next.callbacks, next.imagePaths)
|
|
139
280
|
.then(next.resolve)
|
|
140
281
|
.catch(next.reject);
|
|
141
282
|
}
|
|
@@ -194,10 +335,50 @@ export function killSession(lizardId) {
|
|
|
194
335
|
const killed = session.agent.kill();
|
|
195
336
|
session.isProcessing = false;
|
|
196
337
|
session.queue = [];
|
|
338
|
+
// Remove from map so getActiveSessions() doesn't return dead agents
|
|
339
|
+
sessions.delete(lizardId);
|
|
197
340
|
return killed;
|
|
198
341
|
}
|
|
199
342
|
return false;
|
|
200
343
|
}
|
|
344
|
+
// Revert files to their pre-agent state using the before content from FileChange objects
|
|
345
|
+
export function revertFiles(filePaths, fileChanges) {
|
|
346
|
+
const reverted = [];
|
|
347
|
+
const failed = [];
|
|
348
|
+
const workingDir = getWorkingDir();
|
|
349
|
+
for (const filePath of filePaths) {
|
|
350
|
+
const change = fileChanges.find(fc => fc.filePath === filePath);
|
|
351
|
+
if (!change) {
|
|
352
|
+
failed.push(filePath);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const fullPath = filePath.startsWith('/')
|
|
357
|
+
? filePath
|
|
358
|
+
: path.resolve(workingDir, filePath);
|
|
359
|
+
if (change.before === null) {
|
|
360
|
+
// File was newly created by agent — delete it
|
|
361
|
+
if (existsSync(fullPath)) {
|
|
362
|
+
unlinkSync(fullPath);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// File existed before — restore original content
|
|
367
|
+
const dir = dirname(fullPath);
|
|
368
|
+
if (!existsSync(dir)) {
|
|
369
|
+
mkdirSync(dir, { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
writeFileSync(fullPath, change.before, 'utf-8');
|
|
372
|
+
}
|
|
373
|
+
reverted.push(filePath);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
console.error(`[AgentPool] Failed to revert ${filePath}:`, err);
|
|
377
|
+
failed.push(filePath);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { reverted, failed };
|
|
381
|
+
}
|
|
201
382
|
export function killAllSessions() {
|
|
202
383
|
let count = 0;
|
|
203
384
|
for (const [, session] of sessions) {
|
|
@@ -207,5 +388,7 @@ export function killAllSessions() {
|
|
|
207
388
|
session.isProcessing = false;
|
|
208
389
|
session.queue = [];
|
|
209
390
|
}
|
|
391
|
+
// Clear all sessions so getActiveSessions() returns empty
|
|
392
|
+
sessions.clear();
|
|
210
393
|
return count;
|
|
211
394
|
}
|