agentgui 1.0.680 → 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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git add:*)"
5
+ ]
6
+ }
7
+ }
package/CLAUDE.md CHANGED
@@ -185,26 +185,20 @@ Server broadcasts:
185
185
 
186
186
  ## Steering
187
187
 
188
- Steering sends a follow-up prompt to a running agent via stdin JSON-RPC:
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
- **Process lifetime:** After execution ends, process stays alive 30s (steeringTimeout) for follow-up steers. `conv.steer` resets timeout to another 30s on each steer.
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`, `activeProcessesByConvId`, `messageQueues`, `steeringTimeouts`
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 all maps, kills process, cancels timeout, sets DB isStreaming=0.
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
 
@@ -282,7 +276,7 @@ Speech models (~470MB total) are downloaded automatically on server startup. No
282
276
  - **`all_conversations_deleted`** must be in `BROADCAST_TYPES` set in server.js or it won't fan-out to all clients.
283
277
  - **`streaming_start` and `message_created`** are high-priority in WSOptimizer — they flush immediately, not batched.
284
278
  - **Sidebar animation:** `transition: none !important` in index.html CSS — sidebar snaps instantly on toggle by design.
285
- - **gm plugin requires no `--dangerously-skip-permissions`** flag in claude-runner.js. That flag disables all plugins.
279
+ - **Claude Code always runs with `--dangerously-skip-permissions`** (plugins disabled by design).
286
280
  - **Tool status race on startup:** `autoProvision()` broadcasts `tool_status_update` for already-installed tools so the UI shows correct state before the first manual fetch.
287
281
  - **Thinking blocks** are transient (not in DB), rendered only via `handleStreamingProgress()` in client.js. The `renderEvent` switch case for `thinking_block` is disabled to prevent double-render.
288
282
  - **Terminal output** is base64-encoded (`encoding: 'base64'` field on message). Client decodes with `decodeURIComponent(escape(atob(data)))` pattern for multibyte safety.
@@ -645,6 +645,7 @@ registry.register({
645
645
  if (model) flags.push('--model', model);
646
646
  if (resumeSessionId) flags.push('--resume', resumeSessionId);
647
647
  if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
648
+ flags.push('--dangerously-skip-permissions');
648
649
  flags.push(prompt); // positional arg - stdin stays open separately for steering
649
650
 
650
651
  return flags;
@@ -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, activeProcessesByConvId, steeringTimeouts } = deps;
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
- const proc = (entry && entry.proc) || activeProcessesByConvId.get(p.id);
138
+ if (!entry) fail(409, 'No active execution to steer');
139
139
 
140
- if (proc && proc.stdin && !proc.stdin.destroyed) {
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
- const sessionId = entry?.sessionId;
147
- const promptRequest = {
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
- fail(409, 'Process not available for steering');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.680",
3
+ "version": "1.0.682",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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, activeProcessesByConvId, steeringTimeouts,
4142
- cleanupExecution
4103
+ broadcastSync, processMessageWithStreaming, cleanupExecution
4143
4104
  });
4144
4105
 
4145
4106
  console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.length:', discoveredAgents.length);
@@ -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
- // Fire RPC in background, don't await
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
- try {
541
- const data = await window.wsClient.rpc('conv.inject', { id: this.state.currentConversation.id, content: instructions });
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-steer-btn')) {
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')) {