agentgui 1.0.39 → 1.0.41
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/CLAUDE.md +0 -0
- package/acp-launcher.js +87 -271
- package/package.json +3 -7
- package/server.js +3 -3
- package/stream-handler.js +14 -9
- package/01-initial-load.png +0 -0
- package/AUTOMATIC_IMPORT.md +0 -284
- package/CONVERSATION_DISPLAY_FIX.md +0 -125
- package/DEBUG_GUIDE.md +0 -136
- package/DELIVERABLES.txt +0 -212
- package/DIAGNOSTICS.md +0 -61
- package/FINAL_SUMMARY.md +0 -312
- package/IMPLEMENTATION_CHECKLIST.md +0 -287
- package/IMPLEMENTATION_STATUS.md +0 -189
- package/README.md +0 -213
- package/RECENT_UPDATES.md +0 -209
- package/REMOTE_DEBUG_GUIDE.md +0 -225
- package/RESPONSE_ISSUES.md +0 -157
- package/SIDEBAR_FIX_SUMMARY.md +0 -111
- package/STATE_CONSISTENCY_GUARANTEE.md +0 -183
- package/STATE_CONSISTENCY_TEST_INDEX.md +0 -268
- package/STATE_CONSISTENCY_TEST_REPORT.md +0 -256
- package/STATE_MACHINE_SUMMARY.md +0 -172
- package/TEST_README.md +0 -205
- package/TEST_SUMMARY.md +0 -159
- package/test-artifacts/01-window-a-initial.png +0 -0
- package/test-artifacts/01-window-b-initial.png +0 -0
- package/test-artifacts/02-window-a-after-send.png +0 -0
- package/test-artifacts/02-window-b-after-send.png +0 -0
- package/test-artifacts/snapshot-a-1.txt +0 -1
- package/test-artifacts/snapshot-b-1.txt +0 -1
- package/test-state-consistency.cjs +0 -239
- package/test-state-manager.js +0 -55
package/CLAUDE.md
ADDED
|
File without changes
|
package/acp-launcher.js
CHANGED
|
@@ -1,37 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
|
|
6
|
-
// Common paths where claude-code-acp might be installed
|
|
7
|
-
const CLAUDE_CODE_ACP_PATHS = [
|
|
8
|
-
'/config/.gmweb/npm-global/bin/claude-code-acp',
|
|
9
|
-
'/usr/local/bin/claude-code-acp',
|
|
10
|
-
'/usr/bin/claude-code-acp',
|
|
11
|
-
path.join(os.homedir(), '.local/bin/claude-code-acp'),
|
|
12
|
-
path.join(os.homedir(), '.gmweb/npm-global/bin/claude-code-acp'),
|
|
13
|
-
'claude-code-acp', // fallback to PATH
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
// Common paths where opencode might be installed
|
|
17
|
-
const OPENCODE_PATHS = [
|
|
18
|
-
'/usr/local/bin/opencode',
|
|
19
|
-
'/usr/bin/opencode',
|
|
20
|
-
path.join(os.homedir(), '.local/bin/opencode'),
|
|
21
|
-
'opencode', // fallback to PATH
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function findBinary(paths) {
|
|
25
|
-
for (const p of paths) {
|
|
26
|
-
try {
|
|
27
|
-
fs.accessSync(p, fs.constants.X_OK);
|
|
28
|
-
return p;
|
|
29
|
-
} catch (_) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
1
|
+
import { createClient } from 'claude-code-acp';
|
|
35
2
|
|
|
36
3
|
const RIPPLEUI_SYSTEM_PROMPT = `CRITICAL INSTRUCTION: You are responding in a web-based HTML interface. EVERY response must be formatted as beautiful, styled HTML using RippleUI and Tailwind CSS. This is NOT a text-based interface - users see raw HTML rendered in their browser.
|
|
37
4
|
|
|
@@ -94,289 +61,138 @@ MANDATORY RULES:
|
|
|
94
61
|
✓ Use color classes: text-gray-700, bg-blue-50, border-blue-500
|
|
95
62
|
✓ Make visual hierarchy clear: use different font sizes, colors, cards
|
|
96
63
|
|
|
97
|
-
EXAMPLES OF COMPLETE RESPONSES:
|
|
98
|
-
|
|
99
|
-
Example 1 - Answer:
|
|
100
|
-
<div class="space-y-4 p-6"><h2 class="text-2xl font-bold">Explanation</h2><p class="text-gray-700">Here is the detailed explanation...</p></div>
|
|
101
|
-
|
|
102
|
-
Example 2 - Code:
|
|
103
|
-
<div class="space-y-4 p-6"><h3 class="text-xl font-bold">JavaScript Function</h3><pre class="bg-gray-900 text-white p-4 rounded overflow-x-auto"><code>const greet = () => console.log('Hello');</code></pre></div>
|
|
104
|
-
|
|
105
|
-
Example 3 - Multiple sections:
|
|
106
|
-
<div class="space-y-4 p-6"><h2 class="text-2xl font-bold">Topic</h2><div class="card bg-white shadow p-4"><h3 class="font-bold">Section 1</h3><p>Content here</p></div><div class="card bg-white shadow p-4"><h3 class="font-bold">Section 2</h3><p>More content</p></div></div>
|
|
107
|
-
|
|
108
64
|
YOU MUST ALWAYS OUTPUT VALID, COMPLETE HTML.
|
|
109
65
|
The user's interface shows YOUR HTML directly - make it beautiful, well-organized, and professional.`;
|
|
110
66
|
|
|
111
67
|
export default class ACPConnection {
|
|
112
68
|
constructor() {
|
|
113
|
-
this.
|
|
114
|
-
this.buffer = '';
|
|
115
|
-
this.nextRequestId = 1;
|
|
116
|
-
this.pendingRequests = new Map();
|
|
69
|
+
this.client = null;
|
|
117
70
|
this.sessionId = null;
|
|
118
71
|
this.onUpdate = null;
|
|
119
|
-
this.printMode = false;
|
|
120
|
-
this.cwd = '/config';
|
|
121
72
|
}
|
|
122
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Connect to ACP bridge and create session
|
|
76
|
+
*/
|
|
123
77
|
async connect(agentType, cwd) {
|
|
124
|
-
this.cwd = cwd;
|
|
125
|
-
|
|
126
|
-
const acpSetup = async () => {
|
|
127
|
-
await this._spawnACP(agentType, cwd);
|
|
128
|
-
await this.sendRequest('initialize', {
|
|
129
|
-
protocolVersion: 1,
|
|
130
|
-
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
|
131
|
-
}, 10000);
|
|
132
|
-
const result = await this.sendRequest('session/new', { cwd, mcpServers: [] }, 30000);
|
|
133
|
-
this.sessionId = result.sessionId;
|
|
134
|
-
await this.sendRequest('session/set_mode', { sessionId: this.sessionId, modeId: 'bypassPermissions' }, 10000);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const deadline = new Promise((_, reject) => setTimeout(() => reject(new Error('ACP handshake timeout (60s)')), 60000));
|
|
138
|
-
|
|
139
78
|
try {
|
|
140
|
-
|
|
141
|
-
console.log(`[ACP] Connected via ACP bridge (${agentType})`);
|
|
142
|
-
} catch (acpErr) {
|
|
143
|
-
console.log(`[ACP] Bridge failed: ${acpErr.message}`);
|
|
144
|
-
console.log(`[ACP] Falling back to claude --print mode`);
|
|
145
|
-
this.printMode = true;
|
|
146
|
-
this.sessionId = 'print-' + Date.now();
|
|
147
|
-
if (this.child) {
|
|
148
|
-
try { this.child.kill('SIGTERM'); } catch (_) {}
|
|
149
|
-
this.child = null;
|
|
150
|
-
}
|
|
151
|
-
for (const [id, req] of this.pendingRequests) {
|
|
152
|
-
clearTimeout(req.timeoutId);
|
|
153
|
-
}
|
|
154
|
-
this.pendingRequests.clear();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
_spawnACP(agentType, cwd) {
|
|
159
|
-
return new Promise((resolve, reject) => {
|
|
160
|
-
const env = { ...process.env };
|
|
161
|
-
delete env.NODE_OPTIONS;
|
|
162
|
-
delete env.NODE_INSPECT;
|
|
163
|
-
delete env.NODE_DEBUG;
|
|
164
|
-
|
|
165
|
-
// Ensure npm global bin directories are in PATH
|
|
166
|
-
const npmGlobalBins = [
|
|
167
|
-
'/config/.gmweb/npm-global/bin',
|
|
168
|
-
path.join(os.homedir(), '.gmweb/npm-global/bin'),
|
|
169
|
-
path.join(os.homedir(), '.local/bin'),
|
|
170
|
-
'/usr/local/bin',
|
|
171
|
-
];
|
|
172
|
-
const currentPath = env.PATH || '';
|
|
173
|
-
const newPathEntries = npmGlobalBins.filter(p => !currentPath.includes(p));
|
|
174
|
-
if (newPathEntries.length > 0) {
|
|
175
|
-
env.PATH = [...newPathEntries, currentPath].join(':');
|
|
176
|
-
}
|
|
79
|
+
console.log(`[ACP] Connecting to ${agentType}...`);
|
|
177
80
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
cmd = findBinary(OPENCODE_PATHS);
|
|
183
|
-
args = ['acp'];
|
|
184
|
-
} else {
|
|
185
|
-
cmd = findBinary(CLAUDE_CODE_ACP_PATHS);
|
|
186
|
-
args = [];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!cmd) {
|
|
190
|
-
reject(new Error(`Could not find ${agentType} ACP binary. Please ensure ${agentType === 'opencode' ? 'opencode' : 'claude-code-acp'} is installed and in your PATH.`));
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this.child = spawn(cmd, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env, shell: false });
|
|
195
|
-
} catch (err) {
|
|
196
|
-
reject(new Error(`Failed to spawn ACP: ${err.message}`));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
this.child.stderr.on('data', d => console.error(`[ACP:stderr]`, d.toString().trim()));
|
|
201
|
-
this.child.on('error', err => reject(new Error(`ACP spawn error: ${err.message}`)));
|
|
202
|
-
this.child.on('exit', () => {
|
|
203
|
-
this.child = null;
|
|
204
|
-
for (const [id, req] of this.pendingRequests) {
|
|
205
|
-
req.reject(new Error('ACP process exited'));
|
|
206
|
-
clearTimeout(req.timeoutId);
|
|
207
|
-
}
|
|
208
|
-
this.pendingRequests.clear();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
this.child.stdout.setEncoding('utf8');
|
|
212
|
-
this.child.stdout.on('data', data => {
|
|
213
|
-
this.buffer += data;
|
|
214
|
-
const lines = this.buffer.split('\n');
|
|
215
|
-
this.buffer = lines.pop() || '';
|
|
216
|
-
for (const line of lines) {
|
|
217
|
-
if (!line.trim()) continue;
|
|
218
|
-
try { this.handleMessage(JSON.parse(line)); }
|
|
219
|
-
catch (e) { console.error('[ACP:parse]', line.substring(0, 200), e.message); }
|
|
220
|
-
}
|
|
81
|
+
// Create client directly from npm module
|
|
82
|
+
this.client = await createClient({
|
|
83
|
+
agent: agentType === 'opencode' ? 'opencode' : 'claude-code',
|
|
84
|
+
cwd
|
|
221
85
|
});
|
|
222
86
|
|
|
223
|
-
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
handleMessage(msg) {
|
|
228
|
-
if (msg.method) { this.handleIncoming(msg); return; }
|
|
229
|
-
if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
|
|
230
|
-
const req = this.pendingRequests.get(msg.id);
|
|
231
|
-
this.pendingRequests.delete(msg.id);
|
|
232
|
-
clearTimeout(req.timeoutId);
|
|
233
|
-
if (msg.error) req.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
234
|
-
else req.resolve(msg.result);
|
|
87
|
+
console.log(`[ACP] ✅ Connected to ${agentType} (direct module)`);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`[ACP] ❌ FATAL: Connection failed: ${err.message}`);
|
|
90
|
+
throw new Error(`ACP connection failed for ${agentType}: ${err.message}`);
|
|
235
91
|
}
|
|
236
92
|
}
|
|
237
93
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.resetPromptTimeout();
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (msg.method === 'session/request_permission' && msg.id !== undefined) {
|
|
245
|
-
this.sendResponse(msg.id, { outcome: { outcome: 'selected', optionId: 'allow' } });
|
|
246
|
-
this.resetPromptTimeout();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (msg.method === 'fs/read_text_file' && msg.id !== undefined) {
|
|
250
|
-
try { this.sendResponse(msg.id, { content: fs.readFileSync(msg.params?.path, 'utf-8') }); }
|
|
251
|
-
catch (e) { this.sendError(msg.id, -32000, e.message); }
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
if (msg.method === 'fs/write_text_file' && msg.id !== undefined) {
|
|
255
|
-
try { fs.writeFileSync(msg.params?.path, msg.params?.content, 'utf-8'); this.sendResponse(msg.id, null); }
|
|
256
|
-
catch (e) { this.sendError(msg.id, -32000, e.message); }
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
resetPromptTimeout() {
|
|
262
|
-
for (const [id, req] of this.pendingRequests) {
|
|
263
|
-
if (req.method === 'session/prompt') {
|
|
264
|
-
clearTimeout(req.timeoutId);
|
|
265
|
-
req.timeoutId = setTimeout(() => {
|
|
266
|
-
this.pendingRequests.delete(id);
|
|
267
|
-
req.reject(new Error('session/prompt timeout'));
|
|
268
|
-
}, 300000);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
sendRequest(method, params, timeoutMs = 60000) {
|
|
274
|
-
return new Promise((resolve, reject) => {
|
|
275
|
-
if (!this.child) { reject(new Error('ACP not connected')); return; }
|
|
276
|
-
const id = this.nextRequestId++;
|
|
277
|
-
const timeoutId = setTimeout(() => {
|
|
278
|
-
this.pendingRequests.delete(id);
|
|
279
|
-
reject(new Error(`${method} timeout (${timeoutMs}ms)`));
|
|
280
|
-
}, timeoutMs);
|
|
281
|
-
this.pendingRequests.set(id, { resolve, reject, timeoutId, method });
|
|
282
|
-
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, ...(params && { params }) }) + '\n');
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
sendResponse(id, result) {
|
|
287
|
-
if (!this.child) return;
|
|
288
|
-
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
sendError(id, code, message) {
|
|
292
|
-
if (!this.child) return;
|
|
293
|
-
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n');
|
|
294
|
-
}
|
|
295
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Initialize ACP session
|
|
96
|
+
*/
|
|
296
97
|
async initialize() {
|
|
297
|
-
if (this.
|
|
298
|
-
return this.
|
|
98
|
+
if (!this.client) throw new Error('ACP not connected');
|
|
99
|
+
return this.client.request('initialize', {
|
|
299
100
|
protocolVersion: 1,
|
|
300
|
-
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }
|
|
101
|
+
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }
|
|
301
102
|
});
|
|
302
103
|
}
|
|
303
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Create new session
|
|
107
|
+
*/
|
|
304
108
|
async newSession(cwd) {
|
|
305
|
-
if (this.
|
|
306
|
-
|
|
307
|
-
return { sessionId: this.sessionId };
|
|
308
|
-
}
|
|
309
|
-
const result = await this.sendRequest('session/new', { cwd, mcpServers: [] }, 120000);
|
|
109
|
+
if (!this.client) throw new Error('ACP not connected');
|
|
110
|
+
const result = await this.client.request('session/new', { cwd, mcpServers: [] });
|
|
310
111
|
this.sessionId = result.sessionId;
|
|
311
112
|
return result;
|
|
312
113
|
}
|
|
313
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Set session mode
|
|
117
|
+
*/
|
|
314
118
|
async setSessionMode(modeId) {
|
|
315
|
-
if (this.
|
|
316
|
-
return this.
|
|
119
|
+
if (!this.client) throw new Error('ACP not connected');
|
|
120
|
+
return this.client.request('session/set_mode', { sessionId: this.sessionId, modeId });
|
|
317
121
|
}
|
|
318
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Inject skills and system prompt
|
|
125
|
+
*/
|
|
319
126
|
async injectSkills(additionalContext = '') {
|
|
320
|
-
if (this.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return result;
|
|
332
|
-
} catch (e) {
|
|
333
|
-
// skill_inject may not be supported, try alternative methods
|
|
334
|
-
console.log(`[ACP] skill_inject failed (may not be supported): ${e.message}`);
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
127
|
+
if (!this.client) throw new Error('ACP not connected');
|
|
128
|
+
|
|
129
|
+
const systemPrompt = additionalContext
|
|
130
|
+
? `${RIPPLEUI_SYSTEM_PROMPT}\n\n---\n\n${additionalContext}`
|
|
131
|
+
: RIPPLEUI_SYSTEM_PROMPT;
|
|
132
|
+
|
|
133
|
+
return this.client.request('session/skill_inject', {
|
|
134
|
+
sessionId: this.sessionId,
|
|
135
|
+
skills: [],
|
|
136
|
+
notification: [{ type: 'text', text: systemPrompt }]
|
|
137
|
+
});
|
|
337
138
|
}
|
|
338
139
|
|
|
339
140
|
/**
|
|
340
|
-
* Inject system
|
|
141
|
+
* Inject system context
|
|
341
142
|
*/
|
|
342
143
|
async injectSystemContext() {
|
|
343
|
-
if (this.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
role: 'system'
|
|
351
|
-
});
|
|
352
|
-
return { success: true };
|
|
353
|
-
} catch (e) {
|
|
354
|
-
// This method may not be supported, silently fail
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
144
|
+
if (!this.client) throw new Error('ACP not connected');
|
|
145
|
+
|
|
146
|
+
return this.client.request('session/context', {
|
|
147
|
+
sessionId: this.sessionId,
|
|
148
|
+
context: RIPPLEUI_SYSTEM_PROMPT,
|
|
149
|
+
role: 'system'
|
|
150
|
+
});
|
|
357
151
|
}
|
|
358
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Send prompt and stream updates
|
|
155
|
+
*/
|
|
359
156
|
async sendPrompt(prompt) {
|
|
360
|
-
if (this.
|
|
157
|
+
if (!this.client) throw new Error('ACP not connected');
|
|
158
|
+
|
|
361
159
|
const promptContent = Array.isArray(prompt) ? prompt : [{ type: 'text', text: prompt }];
|
|
362
|
-
return this.sendRequest('session/prompt', { sessionId: this.sessionId, prompt: promptContent }, 300000);
|
|
363
|
-
}
|
|
364
160
|
|
|
365
|
-
|
|
366
|
-
|
|
161
|
+
// Setup update handler before sending
|
|
162
|
+
if (this.onUpdate) {
|
|
163
|
+
this.client.on('update', (update) => {
|
|
164
|
+
// Forward updates immediately with no delay
|
|
165
|
+
this.onUpdate({ update });
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Send prompt and get result
|
|
170
|
+
return this.client.request('session/prompt', {
|
|
171
|
+
sessionId: this.sessionId,
|
|
172
|
+
prompt: promptContent
|
|
173
|
+
}, 300000);
|
|
367
174
|
}
|
|
368
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Check if connection is running
|
|
178
|
+
*/
|
|
369
179
|
isRunning() {
|
|
370
|
-
|
|
371
|
-
return this.child && !this.child.killed;
|
|
180
|
+
return this.client !== null;
|
|
372
181
|
}
|
|
373
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Terminate connection
|
|
185
|
+
*/
|
|
374
186
|
async terminate() {
|
|
375
|
-
if (this.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
187
|
+
if (!this.client) return;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await this.client.close();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(`[ACP] Error during terminate: ${err.message}`);
|
|
193
|
+
} finally {
|
|
194
|
+
this.client = null;
|
|
195
|
+
this.sessionId = null;
|
|
196
|
+
}
|
|
381
197
|
}
|
|
382
198
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -18,15 +18,11 @@
|
|
|
18
18
|
"homepage": "https://github.com/AnEntrypoint/agentgui#readme",
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "node server.js",
|
|
21
|
-
"
|
|
22
|
-
"dev": "node server.js --watch",
|
|
23
|
-
"dev:bun": "bun run server-bun.js --watch",
|
|
24
|
-
"test": "node run-browser-tests.js",
|
|
25
|
-
"test:integration": "./test-integration.sh",
|
|
26
|
-
"test:all": "npm run test:integration && npm run test"
|
|
21
|
+
"dev": "node server.js --watch"
|
|
27
22
|
},
|
|
28
23
|
"dependencies": {
|
|
29
24
|
"better-sqlite3": "^12.6.2",
|
|
25
|
+
"claude-code-acp": "^1.0.0",
|
|
30
26
|
"ws": "^8.14.2"
|
|
31
27
|
}
|
|
32
28
|
}
|
package/server.js
CHANGED
|
@@ -551,10 +551,10 @@ async function processMessage(conversationId, messageId, sessionId, content, age
|
|
|
551
551
|
console.error(`[processMessage] State history: ${JSON.stringify(summary, null, 2)}`);
|
|
552
552
|
|
|
553
553
|
} finally {
|
|
554
|
-
// Cleanup: remove from state store
|
|
555
|
-
|
|
554
|
+
// Cleanup: remove from state store immediately (async to not block)
|
|
555
|
+
setImmediate(() => {
|
|
556
556
|
sessionStateStore.remove(sessionId);
|
|
557
|
-
}
|
|
557
|
+
});
|
|
558
558
|
|
|
559
559
|
// Log final state
|
|
560
560
|
console.log(`[processMessage] Final state: ${stateManager.getState()}`);
|
package/stream-handler.js
CHANGED
|
@@ -62,15 +62,9 @@ export class StreamHandler {
|
|
|
62
62
|
this.sequence = persistedUpdate.sequence;
|
|
63
63
|
this.updateCount++;
|
|
64
64
|
|
|
65
|
-
// Validate consistency after write
|
|
66
|
-
const validation = StateValidator.validateSession(this.sessionId);
|
|
67
|
-
if (!validation.valid) {
|
|
68
|
-
console.error(`[StreamHandler] State validation failed after update:`, validation);
|
|
69
|
-
// Log but continue - database is still source of truth
|
|
70
|
-
}
|
|
71
|
-
|
|
72
65
|
// CRITICAL: Broadcast happens AFTER database write confirms
|
|
73
66
|
// This ensures clients see data that's already persisted
|
|
67
|
+
// Broadcast immediately with zero delay
|
|
74
68
|
this.broadcastFn({
|
|
75
69
|
type: 'stream_update',
|
|
76
70
|
sessionId: this.sessionId,
|
|
@@ -79,8 +73,19 @@ export class StreamHandler {
|
|
|
79
73
|
update: persistedUpdate.content,
|
|
80
74
|
sequence: this.sequence,
|
|
81
75
|
persisted: true,
|
|
82
|
-
timestamp: persistedUpdate.created_at
|
|
83
|
-
|
|
76
|
+
timestamp: persistedUpdate.created_at
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Validate consistency asynchronously (don't block broadcast)
|
|
80
|
+
setImmediate(() => {
|
|
81
|
+
try {
|
|
82
|
+
const validation = StateValidator.validateSession(this.sessionId);
|
|
83
|
+
if (!validation.valid) {
|
|
84
|
+
console.error(`[StreamHandler] State validation failed: ${validation.error}`);
|
|
85
|
+
}
|
|
86
|
+
} catch (validationErr) {
|
|
87
|
+
console.error(`[StreamHandler] Validation error: ${validationErr.message}`);
|
|
88
|
+
}
|
|
84
89
|
});
|
|
85
90
|
} catch (err) {
|
|
86
91
|
console.error(`[StreamHandler] Error persisting update: ${err.message}`);
|
package/01-initial-load.png
DELETED
|
Binary file
|