@yemi33/minions 0.1.2303 → 0.1.2304

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.
@@ -1300,7 +1300,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
1300
1300
  _cleanupStreamDiv();
1301
1301
  if (evt.sessionReset) {
1302
1302
  var resetText;
1303
- if (evt.sessionResetReason === 'runtimeChanged' && evt.previousRuntime && evt.currentRuntime) {
1303
+ if (evt.sessionResetReason === 'idleReap') {
1304
+ resetText = 'Context window cleared after inactivity — fresh session started.';
1305
+ } else if (evt.sessionResetReason === 'runtimeChanged' && evt.previousRuntime && evt.currentRuntime) {
1304
1306
  resetText = 'Runtime switched (' + escHtml(evt.previousRuntime) + ' → ' + escHtml(evt.currentRuntime) + ') — started a fresh session and carried over recent history.';
1305
1307
  } else if (evt.sessionResetReason === 'promptChanged') {
1306
1308
  resetText = 'Minions was updated — started a fresh session and carried over recent history.';
@@ -166,7 +166,8 @@ const RENDER_VERSIONS = {
166
166
  // Bumped to 2 by W-mpmwxkcn000646cc (left-rail tabbed Settings layout).
167
167
  // Bumped to 3 by W-mqk2s8q1 (Advanced → Diagnostics: relocated the diag button).
168
168
  // Bumped to 4 by W-mqrdavob (Agents table: per-agent charter editor column).
169
- settings: 4,
169
+ // Bumped to 5 by W-mr0qs0vw (Worker Pool: CC session idle-timeout field).
170
+ settings: 5,
170
171
  };
171
172
  const _sectionCache = {};
172
173
  const _lastValueByKey = {};
@@ -387,6 +387,7 @@ async function openSettings() {
387
387
  'When ON, a dirty or broken live-checkout (in-place) tree is force-recovered before dispatch via `git fetch origin` + `git reset --hard origin/<branch>` instead of failing the dispatch with non-retryable LIVE_CHECKOUT_DIRTY. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default OFF because the reset is DESTRUCTIVE — it discards the operator\'s uncommitted changes in the live checkout (a `live-checkout-autoreset-<workItem>` inbox note records exactly what was discarded for reflog recovery). Only affects projects running in live/in-place checkout mode.') +
388
388
  '</div>' +
389
389
  '<div class="settings-grid-2">' +
390
+ settingsField('CC Session Idle Timeout', 'set-ccWorkerIdleTimeoutMs', Math.round((e.ccWorkerIdleTimeoutMs || 1800000) / 60000), 'minutes', 'How long a persistent `copilot --acp` CC worker stays warm with no activity before the idle reaper kills it. After a reap the next message cold-spawns a FRESH session with no memory of prior turns and CC shows a "context cleared after inactivity" notice. Shorter = less idle memory/process footprint; longer = more context durability across gaps between messages. Only affects the Copilot worker-pool path (CC Worker Pool ON). Clamped 1–480 minutes.') +
390
391
  settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
391
392
  settingsField('Worktree Create Retries', 'set-worktreeCreateRetries', e.worktreeCreateRetries || 1, '', 'Retry count for transient worktree add failures (0-3)') +
392
393
  settingsField('Worktree Root', 'set-worktreeRoot', e.worktreeRoot || '../worktrees', '', 'Relative or absolute path for git worktrees; on Windows prefer a short path like C:\\wt') +
@@ -1091,6 +1092,14 @@ async function saveSettings() {
1091
1092
  ccModel: (document.getElementById('set-ccModel')?.value ?? '').trim(),
1092
1093
  ccEffort: document.getElementById('set-ccEffort').value || null,
1093
1094
  ccTurnTimeoutMs: document.getElementById('set-ccTurnTimeoutMs')?.value,
1095
+ // Settings UI presents the idle timeout in minutes for readability; the
1096
+ // engine stores ms (ENGINE_DEFAULTS.ccWorkerIdleTimeoutMs). Convert here;
1097
+ // the server clamps to [60000, 28800000] ms. undefined when blank so the
1098
+ // server keeps the prior value rather than zeroing it.
1099
+ ccWorkerIdleTimeoutMs: (function () {
1100
+ var m = Number(document.getElementById('set-ccWorkerIdleTimeoutMs')?.value);
1101
+ return (Number.isFinite(m) && m > 0) ? Math.round(m * 60000) : undefined;
1102
+ })(),
1094
1103
  claudeBareMode: !!document.getElementById('set-claudeBareMode')?.checked,
1095
1104
  claudeFallbackModel: (document.getElementById('set-claudeFallbackModel')?.value ?? '').trim(),
1096
1105
  copilotFallbackModel: (document.getElementById('set-copilotFallbackModel')?.value ?? '').trim(),
@@ -347,6 +347,24 @@
347
347
  scrollToBottom();
348
348
  return div;
349
349
  }
350
+ // W-mr0qs0vw — centered, muted system notice row (e.g. a session reset after
351
+ // the CC worker pool's idle reaper fires). Mirrors the classic dashboard's
352
+ // session-reset banner so both surfaces read identically. Used live from
353
+ // command-send.js and replayed on restore via rerenderHistory.
354
+ function _sessionResetNoticeText(reason) {
355
+ if (reason === 'idleReap') return 'Context window cleared after inactivity — fresh session started.';
356
+ if (reason === 'promptChanged') return 'Minions was updated — fresh session started.';
357
+ if (reason === 'runtimeChanged') return 'Runtime switched — fresh session started.';
358
+ return 'Session reset — fresh session started.';
359
+ }
360
+ function appendSystemNotice(text) {
361
+ var div = document.createElement('div');
362
+ div.className = 'chat-system-notice';
363
+ div.textContent = text;
364
+ _appendMsgEl(div);
365
+ scrollToBottom();
366
+ return div;
367
+ }
350
368
 
351
369
  // ── Queued-message rendering (W-mqayw3x6) ──────────────────────────
352
370
  function _getQueue() {
@@ -617,6 +635,7 @@
617
635
  for (var i = 0; i < messages.length; i++) {
618
636
  var m = messages[i];
619
637
  if (m.role === 'action') appendActionStatus(m.severity || '', m.text || '');
638
+ else if (m.role === 'system') appendSystemNotice(m.text || '');
620
639
  else appendBubble(m.role, m.text || '', m.toolCalls);
621
640
  }
622
641
  }
@@ -190,6 +190,16 @@
190
190
  if (doneEvt.actions && doneEvt.actions.length > 0) {
191
191
  renderActionResults(turnTabId, doneEvt.actions, doneEvt.actionResults || []);
192
192
  }
193
+ // W-mr0qs0vw — the CC worker pool's idle reaper (or a prompt/runtime
194
+ // change) can reset the session between turns. Surface the same centered
195
+ // system notice the classic dashboard shows so slim users aren't left
196
+ // silently losing context. Persist it (role:'system') so it replays on
197
+ // restore; render live only when this turn's tab is the visible one.
198
+ if (doneEvt.sessionReset) {
199
+ var resetMsg = _sessionResetNoticeText(doneEvt.sessionResetReason);
200
+ if (turnTabId === tabId) appendSystemNotice(resetMsg);
201
+ recordTabMessage(turnTabId, { role: 'system', text: resetMsg });
202
+ }
193
203
  // Refresh status + history opportunistically — actions probably
194
204
  // changed something.
195
205
  scheduleStatusRefresh(800);
@@ -898,6 +898,18 @@
898
898
  .chat-action.ok { color: var(--green); }
899
899
  .chat-action.warn { color: var(--amber); }
900
900
  .chat-action.err { color: var(--red); }
901
+ /* W-mr0qs0vw — centered, muted session-reset notice (idle-reap / prompt /
902
+ runtime change). Mirrors the classic dashboard's surface2 banner. */
903
+ .chat-system-notice {
904
+ align-self: center;
905
+ max-width: 90%;
906
+ text-align: center;
907
+ padding: var(--space-1) var(--space-3);
908
+ border-radius: 6px;
909
+ font-size: var(--text-base);
910
+ background: var(--surface2);
911
+ color: var(--muted);
912
+ }
901
913
 
902
914
  /* Lightweight markdown styling inside assistant bubbles. */
903
915
  .chat-msg.assistant code,
package/dashboard.js CHANGED
@@ -254,6 +254,14 @@ function reloadConfig() {
254
254
  CONFIG = queries.getConfig();
255
255
  PROJECTS = _getProjects(CONFIG);
256
256
  ensureConfiguredProjectStateFiles();
257
+ // Keep the ACP worker pool's idle-reaper window in sync with config so a
258
+ // Settings change takes effect without a dashboard restart (W-mr0qs0vw). The
259
+ // pool stays dependency-free at load and relies on this push.
260
+ try {
261
+ const idleMs = Number(CONFIG.engine?.ccWorkerIdleTimeoutMs)
262
+ || shared.ENGINE_DEFAULTS.ccWorkerIdleTimeoutMs;
263
+ ccWorkerPool.setIdleTimeoutMs(idleMs);
264
+ } catch { /* invalid value or pool unavailable — keep prior window */ }
257
265
  }
258
266
  ensureConfiguredProjectStateFiles();
259
267
 
@@ -4676,7 +4684,7 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
4676
4684
  * Why we deliberately skip `docSessions` updates on this path: pool ACP
4677
4685
  * session IDs are valid only inside the live worker process. Persisting
4678
4686
  * them risks the next call seeing a "session unchanged + _docHash
4679
- * matches → skip document content" path after the idle reaper (10 min)
4687
+ * matches → skip document content" path after the idle reaper (30 min)
4680
4688
  * kills the worker, silently starving the new ACP session of the
4681
4689
  * document body. Always re-sending extraContext is correctness-safe; the
4682
4690
  * pool's warm-process saving is preserved regardless.
@@ -5275,7 +5283,7 @@ function _docChatResultLooksSuccessful(result) {
5275
5283
  // usePool: when true (caller is on the worker-pool path), force `existing=null`
5276
5284
  // so the docUnchanged optimization never fires. Pool ACP session IDs are valid
5277
5285
  // only inside the live worker process; when the idle reaper kills the worker
5278
- // (10 min), the server restarts, or the operator flips ccUseWorkerPool with
5286
+ // (30 min), the server restarts, or the operator flips ccUseWorkerPool with
5279
5287
  // prior legacy history on disk, a stale docSessions._docHash matching the
5280
5288
  // current doc would otherwise emit docUnchanged:true → _formatDocChatContext
5281
5289
  // replaces the doc body with "unchanged from the previous turn" → the fresh
@@ -10326,6 +10334,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10326
10334
  tabSessionId = tabEntry.sessionId;
10327
10335
  }
10328
10336
  }
10337
+ // W-mr0qs0vw — if the persistent ACP worker for this tab was killed by
10338
+ // the idle reaper since the last turn, the next message cold-spawns a
10339
+ // fresh session with NO memory of prior turns. Drain the pool's one-shot
10340
+ // reap flag and classify as 'idleReap' so CC surfaces a "context
10341
+ // cleared after inactivity" notice. A stronger reset reason already set
10342
+ // this turn (prompt/runtime change) wins; we still drain so the flag
10343
+ // never lingers into a later turn.
10344
+ let _wasIdleReaped = false;
10345
+ try { _wasIdleReaped = ccWorkerPool.consumeIdleReapNotice(body.tabId || 'default'); } catch { /* pool optional */ }
10346
+ if (_wasIdleReaped && !sessionResetReason) {
10347
+ tabSessionId = null;
10348
+ sessionReset = true;
10349
+ sessionResetReason = 'idleReap';
10350
+ }
10329
10351
  const wasResume = !!tabSessionId;
10330
10352
  const sessionId = tabSessionId || null;
10331
10353
  const resumeNeedsCarryover = wasResume && _ccRuntimeNeedsResumeCarryover(currentRuntime);
@@ -11019,6 +11041,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11019
11041
  // larger models); max 1h (matches CC_CALL_TIMEOUT_MS so the watchdog
11020
11042
  // never outlives the outer abort).
11021
11043
  ccTurnTimeoutMs: [10000, 3600000],
11044
+ // W-mr0qs0vw — ACP worker idle-reaper window. 1min floor (anything
11045
+ // shorter would reap a warm worker between a user's own messages and
11046
+ // thrash cold-spawns); 8h ceiling (a config typo shouldn't pin a warm
11047
+ // copilot process in memory indefinitely). Stored in ms; the Settings
11048
+ // UI presents minutes and converts.
11049
+ ccWorkerIdleTimeoutMs: [60000, 28800000],
11022
11050
  // W-mq9acoo800177bcb — bounded-concurrency for pre-dispatch validator.
11023
11051
  // 1 floor (sequential fallback) and 20 ceiling (above this the LLM
11024
11052
  // provider's per-second rate limits dominate; throughput gains taper).
@@ -31,6 +31,8 @@ Canonical envelope (`_buildCcErrorEnvelope` in `dashboard.js`):
31
31
 
32
32
  **No auto-retry policy.** The backend never re-spawns the LLM after an error envelope. The client never silently resends the user's turn. Retry is a single-click manual action — guards against silent budget burn on `budget-exceeded`, infinite loops on `auth-failure`, and accidental re-charges on `context-limit`. The 429 + reconnect paths (rate-limited fetch retry, SSE reconnect-after-disconnect) remain — those are transport-level, not error-envelope-level.
33
33
 
34
+ **Session-reset notices (`sessionReset` / `sessionResetReason`).** The streaming `done` event can carry `sessionReset: true` with a `sessionResetReason` of `promptChanged` (Minions updated its CC prompt template), `runtimeChanged` (CC runtime switched), or `idleReap` (W-mr0qs0vw — the persistent `copilot --acp` worker was killed by the idle reaper after `engine.ccWorkerIdleTimeoutMs` of inactivity, so the next message cold-spawns a fresh session with no memory). Both classic (`dashboard/js/command-center.js`) and slim (`dashboard/slim/js/command-send.js` + `chat.js#appendSystemNotice`) render a centered, muted system notice; for `idleReap` the text is "Context window cleared after inactivity — fresh session started." The idle-reap flag is a one-shot drained per turn via `ccWorkerPool.consumeIdleReapNotice(tabId)`.
35
+
34
36
  ## Image attachments (PL-cc-image-attachments)
35
37
 
36
38
  Command Center accepts image attachments alongside the text turn. The dashboard paste/drag-drop UI (`dashboard/js/command-center.js`, fed by `command-input.js`) base64-encodes each file and adds an `images` array to the request body. (Doc-Chat does **not** accept images yet — `dashboard/js/modal-qa.js` never sends an `images` array and `handleDocChat`/`ccDocCall` never read one; wiring it is a possible follow-up.)
@@ -36,7 +36,9 @@
36
36
  * - systemPromptHash change → keep proc, send a fresh `session/new`
37
37
  * - model / effort change → no eviction (state updated in place)
38
38
  * - closeTab → `session/cancel` if inflight, then kill proc
39
- * - Idle > 10 min → kill proc (reaper sweep every 60 s)
39
+ * - Idle > timeout → kill proc (reaper sweep every 60 s; window is
40
+ * ENGINE_DEFAULTS.ccWorkerIdleTimeoutMs, default
41
+ * 30 min, pushed in via setIdleTimeoutMs())
40
42
  *
41
43
  * Auth precheck: when `copilot --acp` exits before the initialize handshake
42
44
  * completes, surface
@@ -93,8 +95,14 @@ function _typedError(message, code, retriable = true) {
93
95
  return err;
94
96
  }
95
97
 
96
- // 10 minutes — matches the work-item spec.
97
- const IDLE_REAPER_MS = 10 * 60 * 1000;
98
+ // 30 minutes default — matches ENGINE_DEFAULTS.ccWorkerIdleTimeoutMs. This is
99
+ // now a runtime-configurable knob (W-mr0qs0vw): the pool stays dependency-free
100
+ // at module load (no static `require('./shared')`), so the dashboard pushes the
101
+ // configured value in via `setIdleTimeoutMs()` on every reloadConfig(). Shorter
102
+ // = less idle memory/process footprint; longer = more context durability across
103
+ // gaps between messages.
104
+ const DEFAULT_IDLE_REAPER_MS = 30 * 60 * 1000;
105
+ let _idleReaperMs = DEFAULT_IDLE_REAPER_MS;
98
106
  // Reaper sweep cadence. Not exposed as ENGINE_DEFAULTS to keep the pool
99
107
  // dependency-free; sub-task C/D can plumb a config knob if needed.
100
108
  const REAPER_INTERVAL_MS = 60 * 1000;
@@ -152,6 +160,40 @@ const _internals = {
152
160
  const _tabs = new Map();
153
161
  let _reaperTimer = null;
154
162
 
163
+ // W-mr0qs0vw — idle-reap notice tracking. When `_reapIdleTabs` kills a warm
164
+ // worker for inactivity it records the tabId here (tabId → reapedAt epoch ms).
165
+ // The dashboard's per-turn session-reset detection drains this via
166
+ // `consumeIdleReapNotice(tabId)` so the next message after a reap is classified
167
+ // `sessionResetReason: 'idleReap'` and CC surfaces a "context cleared" notice
168
+ // exactly once. Cleared on explicit closeTab so a deliberate close never
169
+ // masquerades as an idle reap. Survives the subsequent cold-spawn (the user
170
+ // still lost context) — only the consume call or a closeTab clears it.
171
+ const _reapedTabs = new Map();
172
+
173
+ // Update the idle-reaper window at runtime. Called by dashboard.js on every
174
+ // reloadConfig() with `engine.ccWorkerIdleTimeoutMs`. Ignores non-finite /
175
+ // non-positive values so a config typo can't disable reaping or set a negative
176
+ // window; the floor of 1000ms keeps a misconfigured tiny value from thrashing.
177
+ function setIdleTimeoutMs(ms) {
178
+ const n = Number(ms);
179
+ if (!Number.isFinite(n) || n <= 0) return _idleReaperMs;
180
+ _idleReaperMs = Math.max(1000, n);
181
+ return _idleReaperMs;
182
+ }
183
+
184
+ function getIdleTimeoutMs() {
185
+ return _idleReaperMs;
186
+ }
187
+
188
+ // Returns true (and clears the notice) if `tabId`'s warm worker was killed by
189
+ // the idle reaper since the last consume. One-shot: the dashboard calls this
190
+ // once per turn while resolving session-reset state.
191
+ function consumeIdleReapNotice(tabId) {
192
+ if (!_reapedTabs.has(tabId)) return false;
193
+ _reapedTabs.delete(tabId);
194
+ return true;
195
+ }
196
+
155
197
  // CC_POOL_TRACE-gated structured trace logger. Off by default; enable via
156
198
  // `CC_POOL_TRACE=1 minions restart` to dump every getSession lifecycle
157
199
  // transition, stream sessionId capture, and session/update notification
@@ -759,6 +801,10 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
759
801
  }
760
802
 
761
803
  function closeTab(tabId) {
804
+ // An explicit close is not an idle reap — drop any pending reap notice so the
805
+ // next turn doesn't spuriously show the "context cleared after inactivity"
806
+ // banner (W-mr0qs0vw).
807
+ _reapedTabs.delete(tabId);
762
808
  const worker = _tabs.get(tabId);
763
809
  if (!worker) return;
764
810
  _tabs.delete(tabId);
@@ -816,6 +862,7 @@ function shutdown() {
816
862
  try { worker.close(); } catch { /* swallow */ }
817
863
  }
818
864
  _tabs.clear();
865
+ _reapedTabs.clear();
819
866
  if (_reaperTimer) {
820
867
  clearInterval(_reaperTimer);
821
868
  _reaperTimer = null;
@@ -832,8 +879,11 @@ function _reapIdleTabs() {
832
879
  const now = _internals.now();
833
880
  for (const [tabId, worker] of [..._tabs]) {
834
881
  if (worker.inflight) continue;
835
- if (now - worker.lastUsedAt > IDLE_REAPER_MS) {
882
+ if (now - worker.lastUsedAt > _idleReaperMs) {
836
883
  _tabs.delete(tabId);
884
+ // Record the reap so the next turn on this tab can surface a
885
+ // "context cleared after inactivity" notice (W-mr0qs0vw).
886
+ _reapedTabs.set(tabId, now);
837
887
  try { worker.close(); } catch { /* already torn down */ }
838
888
  }
839
889
  }
@@ -845,12 +895,17 @@ module.exports = {
845
895
  closeTab,
846
896
  cancelInflight,
847
897
  shutdown,
898
+ // W-mr0qs0vw — configurable idle-reaper window + idle-reap notice plumbing.
899
+ setIdleTimeoutMs,
900
+ getIdleTimeoutMs,
901
+ consumeIdleReapNotice,
848
902
  // Exposed for unit tests; engine code MUST go through the public API.
849
903
  _internals,
850
904
  _tabs,
905
+ _reapedTabs,
851
906
  _reapIdleTabs,
852
907
  _buildSessionNewParams,
853
- IDLE_REAPER_MS,
908
+ DEFAULT_IDLE_REAPER_MS,
854
909
  REAPER_INTERVAL_MS,
855
910
  WARM_MAX_CONCURRENT,
856
911
  // W-mpmwxni2000c25c7-c — typed-error envelope contract. Exported so the
package/engine/shared.js CHANGED
@@ -3261,6 +3261,7 @@ const ENGINE_DEFAULTS = {
3261
3261
  liveCheckoutAutoReset: false,
3262
3262
  orphanHolderScanTimeoutMs: 5000, // 5s ceiling for the cross-platform holder scan (PowerShell / /proc walk / lsof)
3263
3263
  ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
3264
+ ccWorkerIdleTimeoutMs: 30 * 60 * 1000, // W-mr0qs0vw: idle-reaper window for the persistent `copilot --acp` worker pool (engine/cc-worker-pool.js). After this much inactivity with no in-flight turn the warm ACP process is killed; the next message cold-spawns a fresh session with NO memory of prior turns (CC shows a "context cleared after inactivity" notice). Tradeoff: shorter = less idle memory/process footprint, longer = more context durability across gaps between messages. Wired into the pool via ccWorkerPool.setIdleTimeoutMs() on every reloadConfig(); clamped to [60000, 28800000] (1min–8h) in the settings POST handler.
3264
3265
  ccTurnTimeoutMs: 300000, // W-mpmwxni2000c25c7-b/-d: 5min per-turn no-progress watchdog. The window resets on every liveness signal — token chunk, tool-call notification, tool-update — so an actively-streaming CC/doc-chat turn (long shell command, deep search, sub-agent loop) survives indefinitely up to the outer CC_CALL_TIMEOUT_MS (~1h) ceiling. Only true silence past this window with no progress fires the cancel: the in-flight LLM call is aborted and the handler surfaces `{code:'cc-turn-timeout', retryable:true}` via the typed error envelope so the UI can stop the spinner and offer Retry. Clamped to [10000, 3600000] in the settings POST handler. Independent of CC_CALL_TIMEOUT_MS. Non-streaming doc-chat is the lone wall-clock exception (no progress hooks); see _raceCcDocChatTimeout in dashboard.js for the dual factory/promise shape.
3265
3266
  docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
3266
3267
  ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2303",
3
+ "version": "0.1.2304",
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"