@yemi33/minions 0.1.1931 → 0.1.1933

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/dashboard.js CHANGED
@@ -2480,8 +2480,10 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
2480
2480
  * contract SSE consumers depend on).
2481
2481
  * - `usage` is `{}` because ACP `session/update` notifications don't
2482
2482
  * surface token counts; trackEngineUsage is a no-op on `{}`.
2483
- * - Tool calls are not surfaced (sub-task B/C don't plumb `tool_call`
2484
- * notifications into a callback). Matches CC's pool trade-off.
2483
+ * - Tool calls are surfaced via the optional `onToolUse(name, input)`
2484
+ * callback (ACP `tool_call` notification, mapped to Claude-style
2485
+ * {name, input}). `tool_call_update` (results) is ignored to avoid
2486
+ * double chips.
2485
2487
  * - Honors `timeoutMs`. On timeout: cancels the prompt, closes the tab
2486
2488
  * (so the next call rebuilds against a clean process), resolves with
2487
2489
  * `{ code: 1, stderr: 'doc-chat-pool: timeout after Xms' }`. The
@@ -2496,7 +2498,7 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
2496
2498
  * document body. Always re-sending extraContext is correctness-safe; the
2497
2499
  * pool's warm-process saving is preserved regardless.
2498
2500
  */
2499
- function _invokeDocChatViaPool({ prompt, model, effort, engineConfig, systemPrompt, sessionKey, freshSession, timeoutMs, onChunk }) {
2501
+ function _invokeDocChatViaPool({ prompt, model, effort, engineConfig, systemPrompt, sessionKey, freshSession, timeoutMs, onChunk, onToolUse }) {
2500
2502
  const oneShot = !!freshSession;
2501
2503
  const tabKey = oneShot
2502
2504
  ? 'doc-chat:fresh:' + shared.uid()
@@ -2574,6 +2576,11 @@ function _invokeDocChatViaPool({ prompt, model, effort, engineConfig, systemProm
2574
2576
  try { onChunk(accumulated); } catch { /* swallow */ }
2575
2577
  }
2576
2578
  },
2579
+ onToolUse: (name, input) => {
2580
+ if (onToolUse) {
2581
+ try { onToolUse(name, input || {}); } catch { /* swallow */ }
2582
+ }
2583
+ },
2577
2584
  onDone: () => {
2578
2585
  finalize({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
2579
2586
  },
@@ -2786,7 +2793,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2786
2793
  const p = _invokeDocChatViaPool({
2787
2794
  prompt: poolPrompt, sessionKey, model, effort: ccEffort,
2788
2795
  engineConfig: CONFIG.engine, systemPrompt,
2789
- onChunk,
2796
+ onChunk, onToolUse,
2790
2797
  freshSession, timeoutMs: timeout,
2791
2798
  });
2792
2799
  if (onAbortReady) onAbortReady(p.abort);
@@ -6311,7 +6318,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6311
6318
  */
6312
6319
  function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig, systemPrompt = CC_STATIC_SYSTEM_PROMPT, tabId }) {
6313
6320
  if (shared.resolveCcUseWorkerPool(engineConfig)) {
6314
- return _invokeCcStreamViaPool({ prompt, liveState, model, effort, engineConfig, systemPrompt, tabId });
6321
+ return _invokeCcStreamViaPool({ prompt, liveState, toolUses, model, effort, engineConfig, systemPrompt, tabId });
6315
6322
  }
6316
6323
  const { callLLMStreaming } = require('./engine/llm');
6317
6324
  return callLLMStreaming(prompt, systemPrompt, {
@@ -6345,22 +6352,44 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6345
6352
  * callLLMStreaming's contract is "full accumulated text"; we accumulate
6346
6353
  * here so `liveState.text` and downstream chunk events keep the same
6347
6354
  * semantics consumers already depend on.
6348
- * - Tool calls are not surfaced in sub-task B (the pool ignores
6349
- * `tool_call` notifications). `toolUses` stays empty on this path; if
6350
- * sub-task C/D adds tool_call surfacing in the pool we'll plumb a
6351
- * callback here too.
6355
+ * - Tool calls are surfaced via the pool's `onToolUse` callback (ACP
6356
+ * `tool_call` notification, mapped to Claude-style {name, input} so the
6357
+ * dashboard's existing formatToolSummary chips render unchanged).
6358
+ * `tool_call_update` events (status: completed) are intentionally
6359
+ * ignored — surfacing results too would double the chip count.
6352
6360
  * - `usage` is reported as an empty object — ACP doesn't expose token
6353
6361
  * counts in the in-flight session/update notifications, and the pool's
6354
6362
  * long-lived process makes per-turn usage attribution non-trivial.
6355
6363
  * trackEngineUsage is a no-op on `{}`.
6356
6364
  */
6357
- function _invokeCcStreamViaPool({ prompt, liveState, model, effort, engineConfig, systemPrompt, tabId }) {
6365
+ function _invokeCcStreamViaPool({ prompt, liveState, toolUses, model, effort, engineConfig, systemPrompt, tabId }) {
6358
6366
  const resolvedTabId = tabId || 'default';
6359
6367
  let cancelled = false;
6360
6368
  let accumulated = '';
6361
6369
  let sessionHandle = null;
6362
6370
  let resolveResult;
6371
+ // Per-turn phase timing — emitted as one structured [cc-timing] log line
6372
+ // so we can attribute Copilot CC slowness (e.g. 36s) to spawn vs first-byte
6373
+ // vs streaming vs trailing inference. Parallels engine.js's [spawn-timing].
6374
+ const _tStart = Date.now();
6375
+ let _tFirstByte = null;
6376
+ let _tFirstTool = null;
6377
+ let _chunkCount = 0;
6378
+ let _toolUseCount = 0;
6363
6379
  const promise = new Promise((resolve) => { resolveResult = resolve; });
6380
+ const _emitTimingLog = (lifecycle, sessionReady, streamEnd, outcome) => {
6381
+ try {
6382
+ const _diff = (a, b) => (a != null && b != null) ? (b - a) : null;
6383
+ const timings = {
6384
+ get_session: _diff(_tStart, sessionReady),
6385
+ first_byte: _diff(sessionReady, _tFirstByte),
6386
+ first_tool: _diff(sessionReady, _tFirstTool),
6387
+ stream: _diff(sessionReady, streamEnd),
6388
+ total: _diff(_tStart, streamEnd),
6389
+ };
6390
+ shared.log('info', `[cc-timing] tab=${resolvedTabId} lifecycle=${lifecycle || 'unknown'} outcome=${outcome} chunks=${_chunkCount} tools=${_toolUseCount} ${JSON.stringify(timings)}`);
6391
+ } catch { /* telemetry is best-effort */ }
6392
+ };
6364
6393
  promise.abort = () => {
6365
6394
  cancelled = true;
6366
6395
  try { sessionHandle && sessionHandle.cancel(); } catch { /* swallow */ }
@@ -6375,6 +6404,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6375
6404
  systemPromptHash: _ccPromptHash,
6376
6405
  });
6377
6406
  } catch (err) {
6407
+ _emitTimingLog(null, null, Date.now(), 'spawn-failed');
6378
6408
  return resolveResult({
6379
6409
  text: '',
6380
6410
  sessionId: null,
@@ -6384,22 +6414,38 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6384
6414
  stderr: String((err && err.message) || err || 'cc-worker-pool spawn failed'),
6385
6415
  });
6386
6416
  }
6417
+ const _tSessionReady = Date.now();
6418
+ const _lifecycle = sessionHandle.lifecycle || 'unknown';
6387
6419
  if (cancelled) {
6388
6420
  try { sessionHandle.cancel(); } catch { /* swallow */ }
6421
+ _emitTimingLog(_lifecycle, _tSessionReady, Date.now(), 'cancelled-pre-stream');
6389
6422
  return resolveResult({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
6390
6423
  }
6391
6424
  await sessionHandle.stream(prompt, {
6392
6425
  systemPromptText: systemPrompt,
6393
6426
  onChunk: (delta) => {
6427
+ if (_tFirstByte == null) _tFirstByte = Date.now();
6428
+ _chunkCount += 1;
6394
6429
  accumulated += delta;
6395
6430
  _touchCcLiveStream(liveState);
6396
6431
  liveState.text = accumulated;
6397
6432
  if (liveState.writer) liveState.writer({ type: 'chunk', text: accumulated });
6398
6433
  },
6434
+ onToolUse: (name, input) => {
6435
+ if (_tFirstTool == null) _tFirstTool = Date.now();
6436
+ _toolUseCount += 1;
6437
+ _touchCcLiveStream(liveState);
6438
+ const safeInput = input || {};
6439
+ if (Array.isArray(toolUses)) toolUses.push({ name, input: safeInput });
6440
+ if (Array.isArray(liveState.tools)) liveState.tools.push({ name, input: safeInput });
6441
+ if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(safeInput) });
6442
+ },
6399
6443
  onDone: () => {
6444
+ _emitTimingLog(_lifecycle, _tSessionReady, Date.now(), 'done');
6400
6445
  resolveResult({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
6401
6446
  },
6402
6447
  onError: (err) => {
6448
+ _emitTimingLog(_lifecycle, _tSessionReady, Date.now(), cancelled ? 'cancelled' : 'error');
6403
6449
  resolveResult({
6404
6450
  text: accumulated,
6405
6451
  sessionId: sessionHandle.sessionId,
@@ -251,10 +251,21 @@ class Worker {
251
251
  if (text && this.inflight.onChunk) {
252
252
  try { this.inflight.onChunk(text); } catch { /* swallow */ }
253
253
  }
254
+ } else if (update.sessionUpdate === 'tool_call' && this.inflight.onToolUse) {
255
+ // ACP `tool_call` (status: pending, fired at invocation time) is the
256
+ // pool's equivalent of Claude's tool_use event. We map kinds to
257
+ // Claude-style tool names so the dashboard's existing
258
+ // formatToolSummary (Bash → "$ <cmd>", Read → "Reading <path>", etc.)
259
+ // works unchanged. Status updates (`tool_call_update`, status:
260
+ // completed) carry the result and are ignored here — surfacing
261
+ // results too would double the chip count without adding info the
262
+ // user can act on.
263
+ const mapped = _mapAcpToolCallToToolUse(update);
264
+ if (mapped) {
265
+ try { this.inflight.onToolUse(mapped.name, mapped.input); }
266
+ catch { /* swallow */ }
267
+ }
254
268
  }
255
- // Other update kinds (available_commands_update, tool_call, ...) are
256
- // ignored in sub-task B. Sub-task C/D will surface tool_call to the
257
- // dashboard's onToolUse callback.
258
269
  }
259
270
  }
260
271
 
@@ -279,7 +290,7 @@ class Worker {
279
290
 
280
291
  // ── Stream a single turn ───────────────────────────────────────────────
281
292
  stream(promptText, opts = {}) {
282
- const { onChunk, onDone, onError, signal, systemPromptText } = opts;
293
+ const { onChunk, onToolUse, onDone, onError, signal, systemPromptText } = opts;
283
294
  if (this.killed) {
284
295
  const err = new Error('cc-worker-pool: tab is closed');
285
296
  if (onError) try { onError(err); } catch { /* swallow */ }
@@ -307,6 +318,7 @@ class Worker {
307
318
  id,
308
319
  sessionId: this.sessionId,
309
320
  onChunk,
321
+ onToolUse,
310
322
  onDone,
311
323
  onError,
312
324
  signal,
@@ -425,12 +437,58 @@ function _extractChunkText(content) {
425
437
  return '';
426
438
  }
427
439
 
440
+ // Map an ACP `tool_call` session/update notification to the {name, input} shape
441
+ // the dashboard's formatToolSummary already understands. ACP's `kind` is a
442
+ // coarse category (execute|read|edit|search|fetch|think|other); we translate to
443
+ // the closest Claude tool name so the existing chip formatters keep working
444
+ // (Bash → "$ <cmd>", Read → "Reading <path>", etc.). Unknown kinds fall back
445
+ // to ACP's human-readable `title` with the raw input attached, which renders
446
+ // through the default `<title>(<key>: <val>)` formatter.
447
+ function _mapAcpToolCallToToolUse(update) {
448
+ if (!update || update.sessionUpdate !== 'tool_call') return null;
449
+ const rawInput = (update.rawInput && typeof update.rawInput === 'object') ? update.rawInput : {};
450
+ const kind = String(update.kind || '').toLowerCase();
451
+ const title = update.title || '';
452
+ // For kinds with a clear Claude-tool equivalent, use that name + raw input.
453
+ switch (kind) {
454
+ case 'execute':
455
+ return { name: 'Bash', input: rawInput };
456
+ case 'read':
457
+ return { name: 'Read', input: rawInput };
458
+ case 'edit':
459
+ return { name: 'Edit', input: rawInput };
460
+ case 'search': {
461
+ // Heuristic: Grep needs a pattern; Glob needs a glob pattern.
462
+ // ACP doesn't distinguish, so prefer Grep when a `path` hint is present
463
+ // (matches the dashboard's Grep formatter "Searching <pat> in <path>").
464
+ const isGrep = typeof rawInput.path === 'string' || typeof rawInput.regex === 'string';
465
+ return { name: isGrep ? 'Grep' : 'Glob', input: rawInput };
466
+ }
467
+ case 'fetch':
468
+ return { name: 'WebFetch', input: rawInput };
469
+ case 'think':
470
+ // No equivalent Claude tool; show the title so the user sees Copilot's
471
+ // own description of what it's thinking about.
472
+ return { name: title || 'Think', input: rawInput };
473
+ default:
474
+ // Fallback: show ACP's title and pass rawInput through. The dashboard's
475
+ // default formatter renders this as `<title>(<key>: <val>)`.
476
+ return { name: title || kind || 'Tool', input: rawInput };
477
+ }
478
+ }
479
+
428
480
  // ── Public API ────────────────────────────────────────────────────────────
429
481
 
430
482
  async function getSession({ tabId, model, effort, mcpServers, systemPromptHash, cwd } = {}) {
431
483
  if (!tabId) throw new Error('cc-worker-pool.getSession: tabId is required');
432
484
  const mcpServersHash = _hashMcpServers(mcpServers);
433
485
  let worker = _tabs.get(tabId);
486
+ // Track which lifecycle path we took so the dashboard's [cc-timing] log can
487
+ // attribute warm-reuse vs new-session vs cold-spawn. One of:
488
+ // 'warm-reuse' — proc + session both reused (no spawn, no session/new)
489
+ // 'new-session' — proc reused, fresh session/new (sysprompt hash changed)
490
+ // 'cold-spawn' — fresh proc + initialize + session/new
491
+ let lifecycle = 'warm-reuse';
434
492
 
435
493
  if (worker) {
436
494
  if (worker.killed) {
@@ -449,6 +507,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
449
507
  // caller has rotated in (Bug B / issue #2479).
450
508
  await worker.newSession({ mcpServers, systemPromptHash, model, effort });
451
509
  worker.lastUsedAt = _internals.now();
510
+ lifecycle = 'new-session';
452
511
  } else {
453
512
  // Warm reuse — only update bookkeeping. model/effort changes on a
454
513
  // warm session are tracked here but not pushed via configOptions in
@@ -472,12 +531,14 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
472
531
  try { worker.close(); } catch { /* already torn down */ }
473
532
  throw err;
474
533
  }
534
+ lifecycle = 'cold-spawn';
475
535
  }
476
536
 
477
537
  _ensureReaper();
478
538
 
479
539
  return {
480
540
  sessionId: worker.sessionId,
541
+ lifecycle,
481
542
  stream: (promptText, opts) => worker.stream(promptText, opts),
482
543
  cancel: () => worker.cancel(),
483
544
  close: () => closeTab(tabId),
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-14T03:32:45.485Z"
4
+ "cachedAt": "2026-05-14T03:53:21.722Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1931",
3
+ "version": "0.1.1933",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"