agentgui 1.0.681 → 1.0.682
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 +6 -12
- package/lib/ws-handlers-conv.js +20 -24
- package/package.json +1 -1
- package/server.js +1 -40
- package/static/js/client.js +4 -26
package/CLAUDE.md
CHANGED
|
@@ -185,26 +185,20 @@ Server broadcasts:
|
|
|
185
185
|
|
|
186
186
|
## Steering
|
|
187
187
|
|
|
188
|
-
Steering
|
|
189
|
-
```js
|
|
190
|
-
// conv.steer handler sends to proc.stdin:
|
|
191
|
-
{ jsonrpc: '2.0', id: Date.now(), method: 'session/prompt', params: { sessionId, prompt: [{ type: 'text', text }] } }
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**Process lookup:** `entry.proc` (set by `onProcess` callback on `activeExecutions` entry) OR `activeProcessesByConvId.get(id)`. Check both — race condition between `activeExecutions` being set and `onProcess` firing.
|
|
195
|
-
|
|
196
|
-
**Claude Code stdin:** `supportsStdin: true`, `closeStdin: false` in `lib/claude-runner.js`. Stdin must stay open for steering to work.
|
|
188
|
+
Steering stops the running agent (SIGKILL) and immediately resumes with the new message:
|
|
197
189
|
|
|
198
|
-
|
|
190
|
+
1. `conv.steer` RPC (`ws-handlers-conv.js`) — kills active process, marks session interrupted, creates new user message, calls `startExecution()` to resume
|
|
191
|
+
2. Frontend inject button (`#injectBtn`) — when streaming: reads message input, fires `conv.steer`, clears input
|
|
192
|
+
3. `conv.claudeSessionId` on the conversation row ensures the resumed execution picks up `--resume <sessionId>` automatically
|
|
199
193
|
|
|
200
194
|
## Execution State Management
|
|
201
195
|
|
|
202
196
|
Three parallel state stores (must stay in sync):
|
|
203
|
-
1. **In-memory maps:** `activeExecutions`, `
|
|
197
|
+
1. **In-memory maps:** `activeExecutions`, `messageQueues`
|
|
204
198
|
2. **Database:** `conversations.isStreaming`, `sessions.status`
|
|
205
199
|
3. **WebSocket clients:** `streamingConversations` Set on each client
|
|
206
200
|
|
|
207
|
-
**`cleanupExecution(conversationId)`** — atomic cleanup function in server.js. Always use this, never inline-delete from maps. Clears
|
|
201
|
+
**`cleanupExecution(conversationId)`** — atomic cleanup function in server.js. Always use this, never inline-delete from maps. Clears `activeExecutions`, sets DB isStreaming=0.
|
|
208
202
|
|
|
209
203
|
**Queue drain:** If `processMessageWithStreaming` throws, catch block calls `cleanupExecution` and retries drain after 100ms. Queue never deadlocks.
|
|
210
204
|
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -7,7 +7,7 @@ function expandTilde(p) { return p && p.startsWith('~') ? path.join(os.homedir()
|
|
|
7
7
|
|
|
8
8
|
export function register(router, deps) {
|
|
9
9
|
const { queries, activeExecutions, messageQueues, rateLimitState,
|
|
10
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution
|
|
10
|
+
broadcastSync, processMessageWithStreaming, cleanupExecution } = deps;
|
|
11
11
|
|
|
12
12
|
// Per-conversation queue seq counter for event ordering
|
|
13
13
|
const queueSeqByConv = new Map();
|
|
@@ -135,33 +135,29 @@ export function register(router, deps) {
|
|
|
135
135
|
if (!p.content) fail(400, 'Missing content');
|
|
136
136
|
|
|
137
137
|
const entry = activeExecutions.get(p.id);
|
|
138
|
-
|
|
138
|
+
if (!entry) fail(409, 'No active execution to steer');
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
// Agent is running and stdin is alive — inject prompt directly
|
|
142
|
-
const message = queries.createMessage(p.id, 'user', p.content);
|
|
143
|
-
queries.createEvent('message.created', { role: 'user', messageId: message.id }, p.id);
|
|
144
|
-
broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
|
|
140
|
+
const { pid, sessionId } = entry;
|
|
145
141
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
jsonrpc: '2.0',
|
|
149
|
-
id: Date.now(),
|
|
150
|
-
method: 'session/prompt',
|
|
151
|
-
params: { sessionId, prompt: [{ type: 'text', text: p.content }] }
|
|
152
|
-
};
|
|
153
|
-
proc.stdin.write(JSON.stringify(promptRequest) + '\n');
|
|
154
|
-
|
|
155
|
-
// Reset steering timeout
|
|
156
|
-
const existing = steeringTimeouts.get(p.id);
|
|
157
|
-
if (existing) clearTimeout(existing);
|
|
158
|
-
const t = setTimeout(() => { activeProcessesByConvId.delete(p.id); steeringTimeouts.delete(p.id); }, 30000);
|
|
159
|
-
steeringTimeouts.set(p.id, t);
|
|
160
|
-
|
|
161
|
-
return { ok: true, steered: true, conversationId: p.id, messageId: message.id };
|
|
142
|
+
if (pid) {
|
|
143
|
+
try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch (e) {} }
|
|
162
144
|
}
|
|
163
145
|
|
|
164
|
-
|
|
146
|
+
if (sessionId) queries.updateSession(sessionId, { status: 'interrupted', completed_at: Date.now() });
|
|
147
|
+
queries.setIsStreaming(p.id, false);
|
|
148
|
+
activeExecutions.delete(p.id);
|
|
149
|
+
|
|
150
|
+
broadcastSync({ type: 'streaming_complete', sessionId, conversationId: p.id, interrupted: true, timestamp: Date.now() });
|
|
151
|
+
|
|
152
|
+
const agentId = conv.agentType || conv.agentId || 'claude-code';
|
|
153
|
+
const model = conv.model || null;
|
|
154
|
+
const subAgent = conv.subAgent || null;
|
|
155
|
+
const message = queries.createMessage(p.id, 'user', p.content);
|
|
156
|
+
queries.createEvent('message.created', { role: 'user', messageId: message.id }, p.id);
|
|
157
|
+
broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
|
|
158
|
+
startExecution(p.id, message, agentId, model, p.content, subAgent);
|
|
159
|
+
|
|
160
|
+
return { ok: true, steered: true, conversationId: p.id, messageId: message.id };
|
|
165
161
|
});
|
|
166
162
|
|
|
167
163
|
router.handle('msg.ls', (p) => {
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -299,8 +299,6 @@ const activeScripts = new Map();
|
|
|
299
299
|
const messageQueues = new Map();
|
|
300
300
|
const rateLimitState = new Map();
|
|
301
301
|
const activeProcessesByRunId = new Map();
|
|
302
|
-
const activeProcessesByConvId = new Map(); // Store process handles by conversationId for steering
|
|
303
|
-
const steeringTimeouts = new Map(); // Track timeout handles for process cleanup
|
|
304
302
|
const checkpointManager = new CheckpointManager(queries);
|
|
305
303
|
const STUCK_AGENT_THRESHOLD_MS = 1800000;
|
|
306
304
|
const NO_PID_GRACE_PERIOD_MS = 60000;
|
|
@@ -315,26 +313,7 @@ const debugLog = (msg) => {
|
|
|
315
313
|
function cleanupExecution(conversationId, broadcastCompletion = false) {
|
|
316
314
|
debugLog(`[cleanup] Starting cleanup for ${conversationId}`);
|
|
317
315
|
|
|
318
|
-
// Clean in-memory maps in atomic block
|
|
319
316
|
activeExecutions.delete(conversationId);
|
|
320
|
-
const proc = activeProcessesByConvId.get(conversationId);
|
|
321
|
-
activeProcessesByConvId.delete(conversationId);
|
|
322
|
-
|
|
323
|
-
// Cancel steering timeout if present
|
|
324
|
-
const steeringTimeout = steeringTimeouts.get(conversationId);
|
|
325
|
-
if (steeringTimeout) {
|
|
326
|
-
clearTimeout(steeringTimeout);
|
|
327
|
-
steeringTimeouts.delete(conversationId);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Try to kill process if still alive
|
|
331
|
-
if (proc) {
|
|
332
|
-
try {
|
|
333
|
-
if (proc.kill) proc.kill('SIGTERM');
|
|
334
|
-
} catch (e) {
|
|
335
|
-
debugLog(`[cleanup] Error killing process: ${e.message}`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
317
|
|
|
339
318
|
// Clean database state
|
|
340
319
|
queries.setIsStreaming(conversationId, false);
|
|
@@ -3724,8 +3703,6 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3724
3703
|
if (entry) entry.pid = pid;
|
|
3725
3704
|
},
|
|
3726
3705
|
onProcess: (proc) => {
|
|
3727
|
-
// Store process handle for steering - both maps so steer handler always finds it
|
|
3728
|
-
activeProcessesByConvId.set(conversationId, proc);
|
|
3729
3706
|
const entry = activeExecutions.get(conversationId);
|
|
3730
3707
|
if (entry) entry.proc = proc;
|
|
3731
3708
|
}
|
|
@@ -3736,17 +3713,10 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3736
3713
|
// Check if rate limit was already handled in stream detection
|
|
3737
3714
|
if (rateLimitState.get(conversationId)?.isStreamDetected) {
|
|
3738
3715
|
debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping success handler`);
|
|
3739
|
-
activeProcessesByConvId.delete(conversationId);
|
|
3740
3716
|
return;
|
|
3741
3717
|
}
|
|
3742
3718
|
|
|
3743
3719
|
activeExecutions.delete(conversationId);
|
|
3744
|
-
// Keep process alive for steering for up to 30 seconds after execution completes
|
|
3745
|
-
const steeringTimeout = setTimeout(() => {
|
|
3746
|
-
activeProcessesByConvId.delete(conversationId);
|
|
3747
|
-
steeringTimeouts.delete(conversationId);
|
|
3748
|
-
}, 30000);
|
|
3749
|
-
steeringTimeouts.set(conversationId, steeringTimeout);
|
|
3750
3720
|
batcher.drain();
|
|
3751
3721
|
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
3752
3722
|
|
|
@@ -3909,14 +3879,6 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3909
3879
|
if (!rateLimitState.has(conversationId)) {
|
|
3910
3880
|
cleanupExecution(conversationId);
|
|
3911
3881
|
drainMessageQueue(conversationId);
|
|
3912
|
-
} else {
|
|
3913
|
-
// Rate limit in flight - keep execution entry for now, but clean process handle
|
|
3914
|
-
activeProcessesByConvId.delete(conversationId);
|
|
3915
|
-
const steeringTimeout = steeringTimeouts.get(conversationId);
|
|
3916
|
-
if (steeringTimeout) {
|
|
3917
|
-
clearTimeout(steeringTimeout);
|
|
3918
|
-
steeringTimeouts.delete(conversationId);
|
|
3919
|
-
}
|
|
3920
3882
|
}
|
|
3921
3883
|
}
|
|
3922
3884
|
}
|
|
@@ -4138,8 +4100,7 @@ const wsRouter = new WsRouter();
|
|
|
4138
4100
|
|
|
4139
4101
|
registerConvHandlers(wsRouter, {
|
|
4140
4102
|
queries, activeExecutions, messageQueues, rateLimitState,
|
|
4141
|
-
broadcastSync, processMessageWithStreaming,
|
|
4142
|
-
cleanupExecution
|
|
4103
|
+
broadcastSync, processMessageWithStreaming, cleanupExecution
|
|
4143
4104
|
});
|
|
4144
4105
|
|
|
4145
4106
|
console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.length:', discoveredAgents.length);
|
package/static/js/client.js
CHANGED
|
@@ -520,16 +520,14 @@ class AgentGUIClient {
|
|
|
520
520
|
return;
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
// Capture message and clear UI immediately (no await)
|
|
524
523
|
const steerMsg = message;
|
|
525
524
|
if (this.ui.messageInput) {
|
|
526
525
|
this.ui.messageInput.value = '';
|
|
527
526
|
this.ui.messageInput.style.height = 'auto';
|
|
528
527
|
}
|
|
529
528
|
|
|
530
|
-
//
|
|
529
|
+
// Stop agent and resume with new message
|
|
531
530
|
window.wsClient.rpc('conv.steer', { id: this.state.currentConversation.id, content: steerMsg })
|
|
532
|
-
.then(data => console.log('Steer response:', data))
|
|
533
531
|
.catch(err => {
|
|
534
532
|
console.error('Failed to steer:', err);
|
|
535
533
|
this.showError('Failed to steer: ' + err.message);
|
|
@@ -537,12 +535,8 @@ class AgentGUIClient {
|
|
|
537
535
|
} else {
|
|
538
536
|
const instructions = await window.UIDialog.prompt('Enter instructions to inject into the running agent:', '', 'Inject Instructions');
|
|
539
537
|
if (!instructions) return;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.log('Inject response:', data);
|
|
543
|
-
} catch (err) {
|
|
544
|
-
console.error('Failed to inject:', err);
|
|
545
|
-
}
|
|
538
|
+
window.wsClient.rpc('conv.inject', { id: this.state.currentConversation.id, content: instructions })
|
|
539
|
+
.catch(err => console.error('Failed to inject:', err));
|
|
546
540
|
}
|
|
547
541
|
});
|
|
548
542
|
}
|
|
@@ -1409,11 +1403,9 @@ class AgentGUIClient {
|
|
|
1409
1403
|
outputEl.appendChild(queueEl);
|
|
1410
1404
|
}
|
|
1411
1405
|
|
|
1412
|
-
const isStreaming = this.state.streamingConversations.has(conversationId);
|
|
1413
1406
|
queueEl.innerHTML = queue.map((q, i) => `
|
|
1414
1407
|
<div class="queue-item" data-message-id="${q.messageId}" style="padding:0.5rem 1rem;margin:0.5rem 0;border-radius:0.375rem;background:var(--color-warning);color:#000;font-size:0.875rem;display:flex;align-items:center;gap:0.5rem;">
|
|
1415
1408
|
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${i + 1}. ${this.escapeHtml(q.content)}</span>
|
|
1416
|
-
${isStreaming ? `<button class="queue-steer-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:#06b6d4;border:1px solid #0891b2;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;color:#fff;">Steer</button>` : ''}
|
|
1417
1409
|
<button class="queue-edit-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Edit</button>
|
|
1418
1410
|
<button class="queue-delete-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Delete</button>
|
|
1419
1411
|
</div>
|
|
@@ -1422,21 +1414,7 @@ class AgentGUIClient {
|
|
|
1422
1414
|
if (!queueEl._listenersAttached) {
|
|
1423
1415
|
queueEl._listenersAttached = true;
|
|
1424
1416
|
queueEl.addEventListener('click', async (e) => {
|
|
1425
|
-
if (e.target.classList.contains('queue-
|
|
1426
|
-
const index = parseInt(e.target.dataset.index);
|
|
1427
|
-
const q = queue[index];
|
|
1428
|
-
try {
|
|
1429
|
-
const data = await window.wsClient.rpc('conv.steer', { id: conversationId, content: q.content });
|
|
1430
|
-
console.log('Steer response:', data);
|
|
1431
|
-
if (data.ok && data.steered) {
|
|
1432
|
-
// Remove from queue after successful steer
|
|
1433
|
-
await window.wsClient.rpc('q.del', { id: conversationId, messageId: q.messageId });
|
|
1434
|
-
}
|
|
1435
|
-
} catch (err) {
|
|
1436
|
-
console.error('Failed to steer:', err);
|
|
1437
|
-
this.showError('Failed to steer: ' + err.message);
|
|
1438
|
-
}
|
|
1439
|
-
} else if (e.target.classList.contains('queue-delete-btn')) {
|
|
1417
|
+
if (e.target.classList.contains('queue-delete-btn')) {
|
|
1440
1418
|
const index = parseInt(e.target.dataset.index);
|
|
1441
1419
|
const msgId = queue[index].messageId;
|
|
1442
1420
|
if (await window.UIDialog.confirm('Delete this queued message?', 'Delete Message')) {
|