@yemi33/minions 0.1.2045 → 0.1.2047

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.
@@ -522,6 +522,27 @@ function parseStreamChunk(line) {
522
522
 
523
523
  // ── Error Normalization ──────────────────────────────────────────────────────
524
524
 
525
+ // Pull a model id out of an Anthropic-style invalid-model error string. Claude
526
+ // CLI surfaces these as either plain stderr ("Unknown model: claude-x") or as
527
+ // the API's structured error body ({"type":"error","error":{"message":
528
+ // "model: claude-x: not a valid model"}}). Returns null when no obvious model
529
+ // token is present — parseError falls back to "unknown" in that case.
530
+ function _extractInvalidModelName(text) {
531
+ if (!text) return null;
532
+ const patterns = [
533
+ /(?:unknown|invalid)\s+model(?:\s+id)?\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
534
+ /model\s+['"`]([^'"`]+)['"`]\s+(?:not\s+found|is\s+invalid|is\s+unknown|invalid)/i,
535
+ /model\s*[:=]\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?\s*[:,]\s*(?:not\s+a\s+valid\s+model|not\s+found|invalid)/i,
536
+ /model\s+(?:not\s+found|invalid|unknown)\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
537
+ /"model"\s*:\s*"([^"]+)"/i,
538
+ ];
539
+ for (const re of patterns) {
540
+ const m = text.match(re);
541
+ if (m && m[1]) return m[1];
542
+ }
543
+ return null;
544
+ }
545
+
525
546
  /**
526
547
  * Inspect raw agent output (stdout/stderr concatenated by the caller) and map
527
548
  * common Claude error patterns onto a normalized shape:
@@ -531,6 +552,11 @@ function parseStreamChunk(line) {
531
552
  * - 'auth-failure' — invalid API key / credit-card / org-blocked
532
553
  * - 'context-limit' — context window exhausted
533
554
  * - 'budget-exceeded' — `--max-budget-usd` ceiling hit
555
+ * - 'model-unavailable' — model not available. Two flavors distinguished by
556
+ * `retriable`: retriable=true for transient upstream
557
+ * overload (503/overloaded_error — engine retries
558
+ * with `--fallback-model`); retriable=false for an
559
+ * invalid/typo'd model id (user must fix config).
534
560
  * - 'crash' — CLI crashed (segfault, panic, "Internal error")
535
561
  * - null — no recognised pattern
536
562
  *
@@ -553,6 +579,20 @@ function parseError(rawOutput) {
553
579
  if (/budget.*exceed|max.budget.usd.*reach|cost.*limit.*exceed/i.test(lower)) {
554
580
  return { message: 'Claude budget cap exceeded — check your Claude account spending limit.', code: 'budget-exceeded', retriable: false };
555
581
  }
582
+ // W-mpmwxni2000c25c7-a — Configured model isn't a valid Anthropic id (typo,
583
+ // deprecated id, invalid_request_error on `model:` field). Claude has no
584
+ // listModels() catalog (capabilities.modelDiscovery === false), so the
585
+ // message points the operator at Settings instead of enumerating ids.
586
+ // Match BEFORE the overload branch — the overload regex matches
587
+ // `model.*unavailable` and would otherwise swallow "model X is invalid".
588
+ if (/unknown\s+model|model\s+not\s+found|invalid\s+model|model_not_found|not\s+a\s+valid\s+model|400[^a-z]+(?:bad\s+request|invalid|model)/i.test(text)) {
589
+ const name = _extractInvalidModelName(text) || 'unknown';
590
+ return {
591
+ message: `Model "${name}" not available for runtime claude. Configure a valid model in Settings → Engine.`,
592
+ code: 'model-unavailable',
593
+ retriable: false,
594
+ };
595
+ }
556
596
  // W-mpg6isvy000xca4d — Anthropic overload / 503 / service-unavailable. Claude's
557
597
  // own `--fallback-model` only fires on 429 (rate-limit); these failure modes
558
598
  // hang the agent until the 5h timeout. Classify as MODEL_UNAVAILABLE so the
@@ -828,5 +868,6 @@ module.exports = {
828
868
  permissionBypassFlags: PERMISSION_BYPASS_FLAGS,
829
869
  // Exposed for unit tests — never imported by engine code
830
870
  _CLAUDE_SHORTHANDS,
871
+ _extractInvalidModelName,
831
872
  THINKING_BLOCK_TYPES,
832
873
  };
@@ -785,6 +785,58 @@ function parseStreamChunk(line) {
785
785
 
786
786
  // ── Error Normalization ─────────────────────────────────────────────────────
787
787
 
788
+ // In-memory model-discovery cache used by parseError's invalid-model branch
789
+ // (W-mpmwxni2000c25c7-a). parseError is synchronous — to enrich the error
790
+ // message with the live model catalog without adding a per-error HTTP round
791
+ // trip, the engine pre-warms this cache via `_warmModelCache()` (e.g. during
792
+ // preflight or the first listModels() call from the dashboard). Concurrent
793
+ // callers share the in-flight promise so the API is only hit once even when
794
+ // multiple agents fail simultaneously. `_modelDiscoveryResults` stores the
795
+ // resolved catalog (or `[]` on failure); parseError reads it synchronously.
796
+ let _modelDiscoveryResults = null;
797
+ let _modelDiscoveryPromise = null;
798
+
799
+ async function _warmModelCache(opts = {}) {
800
+ if (_modelDiscoveryPromise) return _modelDiscoveryPromise;
801
+ if (Array.isArray(_modelDiscoveryResults)) return _modelDiscoveryResults;
802
+ _modelDiscoveryPromise = listModels(opts).then(
803
+ (models) => { _modelDiscoveryResults = Array.isArray(models) ? models : []; return _modelDiscoveryResults; },
804
+ () => { _modelDiscoveryResults = []; return _modelDiscoveryResults; },
805
+ );
806
+ try {
807
+ return await _modelDiscoveryPromise;
808
+ } finally {
809
+ _modelDiscoveryPromise = null;
810
+ }
811
+ }
812
+
813
+ function _resetModelCache({ models = null } = {}) {
814
+ _modelDiscoveryResults = Array.isArray(models) ? models : (models === null ? null : []);
815
+ _modelDiscoveryPromise = null;
816
+ }
817
+
818
+ // Pull a model id out of a Copilot/Anthropic-style invalid-model error string.
819
+ // Handles the shapes observed in the wild:
820
+ // "Unknown model: banana"
821
+ // "model not found: gpt-5.4"
822
+ // "model 'gpt-x' is invalid"
823
+ // "Invalid model id: claude-sonnet"
824
+ // {"error":"model_not_found","model":"gpt-x"} / {"model":"gpt-x"}
825
+ function _extractInvalidModelName(text) {
826
+ if (!text) return null;
827
+ const patterns = [
828
+ /(?:unknown|invalid)\s+model(?:\s+id)?\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
829
+ /model\s+['"`]([^'"`]+)['"`]\s+(?:not\s+found|is\s+invalid|is\s+unknown|invalid)/i,
830
+ /model\s+(?:not\s+found|invalid|unknown)\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
831
+ /"model"\s*:\s*"([^"]+)"/i,
832
+ ];
833
+ for (const re of patterns) {
834
+ const m = text.match(re);
835
+ if (m && m[1]) return m[1];
836
+ }
837
+ return null;
838
+ }
839
+
788
840
  function _collectErrorSignal(rawOutput) {
789
841
  const text = rawOutput == null ? '' : String(rawOutput);
790
842
  if (!text) return '';
@@ -835,6 +887,27 @@ function _collectErrorSignal(rawOutput) {
835
887
  return sawJsonLine ? '' : text;
836
888
  }
837
889
 
890
+ function _readModelIdsFromDiskCache() {
891
+ try {
892
+ const text = fs.readFileSync(MODELS_CACHE, 'utf8');
893
+ const obj = JSON.parse(text);
894
+ if (!obj || !Array.isArray(obj.models)) return null;
895
+ const ids = obj.models.map(m => m && m.id ? String(m.id) : null).filter(Boolean);
896
+ return ids.length > 0 ? ids : null;
897
+ } catch { return null; }
898
+ }
899
+
900
+ function _resolveCachedModelIds() {
901
+ // In-memory cache (seeded by `_warmModelCache()` or test `_resetModelCache`)
902
+ // wins over disk so unit tests stay hermetic. Production code that hasn't
903
+ // warmed the in-memory cache still gets the catalog via the disk file that
904
+ // `engine/model-discovery.js#getRuntimeModels` populates during preflight.
905
+ if (Array.isArray(_modelDiscoveryResults) && _modelDiscoveryResults.length > 0) {
906
+ return _modelDiscoveryResults.map(m => m && m.id ? String(m.id) : null).filter(Boolean);
907
+ }
908
+ return _readModelIdsFromDiskCache();
909
+ }
910
+
838
911
  function parseError(rawOutput) {
839
912
  const text = _collectErrorSignal(rawOutput);
840
913
  if (!text) return { message: '', code: null, retriable: true };
@@ -845,6 +918,24 @@ function parseError(rawOutput) {
845
918
  if (hasExplicitAuthFailure || hasAuthStatusCode) {
846
919
  return { message: text, code: 'auth-failure', retriable: false };
847
920
  }
921
+ // W-mpmwxni2000c25c7-a — Configured model isn't in the Copilot catalog
922
+ // (typo, deprecated id, 400-style invalid-model response). Classify as
923
+ // MODEL_UNAVAILABLE with retriable: false so the engine surfaces an
924
+ // actionable error instead of looping the dispatch. Must match BEFORE the
925
+ // overload branch — the overload regex catches `model.*unavailable`,
926
+ // which would otherwise swallow legitimate "model not found" responses.
927
+ if (/unknown\s+model|model\s+not\s+found|invalid\s+model|model_not_found|400[^a-z]+(?:bad\s+request|invalid|model)|model.*\b400\b/i.test(text)) {
928
+ const name = _extractInvalidModelName(text) || 'unknown';
929
+ const ids = _resolveCachedModelIds();
930
+ const tail = (ids && ids.length > 0)
931
+ ? `Available models: ${ids.join(', ')}`
932
+ : 'Configure a valid model in Settings → Engine.';
933
+ return {
934
+ message: `Model "${name}" not available for runtime copilot. ${tail}`,
935
+ code: 'model-unavailable',
936
+ retriable: false,
937
+ };
938
+ }
848
939
  // W-mpg6isvy000xca4d — Copilot has no --fallback-model flag; classify
849
940
  // overloaded / 503 / service_unavailable as MODEL_UNAVAILABLE so the engine
850
941
  // retry can OVERRIDE --model with engine.copilotFallbackModel. Match before
@@ -856,9 +947,6 @@ function parseError(rawOutput) {
856
947
  if (/rate limit|too many requests|\b429\b/i.test(text)) {
857
948
  return { message: text, code: 'rate-limit', retriable: true };
858
949
  }
859
- if (/unknown model|model not found|model.*invalid|invalid model/i.test(text)) {
860
- return { message: text, code: 'unknown-model', retriable: false };
861
- }
862
950
  if (/budget.*exceed|premium.*limit.*reach|quota.*exceed/i.test(lower)) {
863
951
  return { message: text, code: 'budget-exceeded', retriable: false };
864
952
  }
@@ -1198,6 +1286,12 @@ module.exports = {
1198
1286
  _pickStandaloneCopilotFromOutput,
1199
1287
  _resolveNpmCopilotJsEntry,
1200
1288
  _isCachedBinUsable,
1289
+ // W-mpmwxni2000c25c7-a — invalid-model error-path helpers. `_warmModelCache`
1290
+ // populates the in-memory model catalog so parseError can enrich its
1291
+ // "Model X not available" message without a per-error HTTP round trip.
1292
+ _warmModelCache,
1293
+ _resetModelCache,
1294
+ _extractInvalidModelName,
1201
1295
  CAPS_SCHEMA_VERSION,
1202
1296
  KNOWN_EVENT_TYPES,
1203
1297
  };
package/engine/shared.js CHANGED
@@ -1856,7 +1856,7 @@ const ENGINE_DEFAULTS = {
1856
1856
  // Engine code MUST go through the resolveAgent*/resolveCc* helpers below;
1857
1857
  // never read these fields directly. New runtimes are added by registering
1858
1858
  // an adapter in engine/runtimes/index.js — these defaults stay stable.
1859
- defaultCli: 'claude', // fleet-wide CLI runtime (must be a key in engine/runtimes/index.js)
1859
+ defaultCli: 'copilot', // fleet-wide CLI runtime (must be a key in engine/runtimes/index.js); flipped from 'claude' in W-mpmwxkk40007c995 — Copilot is now the primary runtime, Claude remains supported as an opt-in
1860
1860
  defaultModel: undefined, // fleet-wide model; undefined = let the runtime adapter pick its own default
1861
1861
  ccCli: undefined, // CC/doc-chat CLI override; undefined = inherit defaultCli (independent of agent path)
1862
1862
  ccModel: undefined, // CC/doc-chat model override; undefined = inherit defaultModel
@@ -1879,6 +1879,7 @@ const ENGINE_DEFAULTS = {
1879
1879
  removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
1880
1880
  removeWorktreeFailureMaxEntries: 1000, // bound failed-worktree retry suppression cache
1881
1881
  ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
1882
+ ccTurnTimeoutMs: 300000, // W-mpmwxni2000c25c7-b: wall-clock cap per CC/doc-chat turn; on expiry the in-flight LLM call is aborted and the handler surfaces `{code:'cc-turn-timeout', retriable:true}` instead of hanging the UI
1882
1883
  docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
1883
1884
  ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
1884
1885
  metricsFlushIntervalMs: 10000, // batch trackEngineUsage writes to metrics.json — flushed every 10s instead of per-call to cut lock contention and dashboard mtime churn
@@ -2084,7 +2085,7 @@ function _isMeaningful(v) {
2084
2085
  * Resolve the CLI runtime for a per-agent spawn. Priority:
2085
2086
  * 1. `agent.cli` — per-agent override
2086
2087
  * 2. `engine.defaultCli` — fleet default
2087
- * 3. `ENGINE_DEFAULTS.defaultCli` ('claude') — hardcoded fallback
2088
+ * 3. `ENGINE_DEFAULTS.defaultCli` ('copilot') — hardcoded fallback
2088
2089
  *
2089
2090
  * Does NOT fall through to `engine.ccCli`. CC and agents are independent paths.
2090
2091
  */
@@ -2098,7 +2099,7 @@ function resolveAgentCli(agent, engine) {
2098
2099
  * Resolve the CLI runtime for the Command Center / doc-chat. Priority:
2099
2100
  * 1. `engine.ccCli` — CC-only override
2100
2101
  * 2. `engine.defaultCli` — fleet default
2101
- * 3. `ENGINE_DEFAULTS.defaultCli` ('claude') — hardcoded fallback
2102
+ * 3. `ENGINE_DEFAULTS.defaultCli` ('copilot') — hardcoded fallback
2102
2103
  *
2103
2104
  * Does NOT inspect any agent overrides. CC has no notion of "which agent" —
2104
2105
  * it's a fleet-wide singleton.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2045",
3
+ "version": "0.1.2047",
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"