kernelbot 1.0.39 → 1.0.40

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.
Files changed (80) hide show
  1. package/bin/kernel.js +5 -5
  2. package/config.example.yaml +1 -1
  3. package/package.json +1 -1
  4. package/skills/business/business-analyst.md +32 -0
  5. package/skills/business/product-manager.md +32 -0
  6. package/skills/business/project-manager.md +32 -0
  7. package/skills/business/startup-advisor.md +32 -0
  8. package/skills/creative/music-producer.md +32 -0
  9. package/skills/creative/photographer.md +32 -0
  10. package/skills/creative/video-producer.md +32 -0
  11. package/skills/data/bi-analyst.md +37 -0
  12. package/skills/data/data-scientist.md +38 -0
  13. package/skills/data/ml-engineer.md +38 -0
  14. package/skills/design/graphic-designer.md +38 -0
  15. package/skills/design/product-designer.md +41 -0
  16. package/skills/design/ui-ux.md +38 -0
  17. package/skills/education/curriculum-designer.md +32 -0
  18. package/skills/education/language-teacher.md +32 -0
  19. package/skills/education/tutor.md +32 -0
  20. package/skills/engineering/data-eng.md +55 -0
  21. package/skills/engineering/devops.md +56 -0
  22. package/skills/engineering/mobile-dev.md +55 -0
  23. package/skills/engineering/security-eng.md +55 -0
  24. package/skills/engineering/sr-backend.md +55 -0
  25. package/skills/engineering/sr-frontend.md +55 -0
  26. package/skills/finance/accountant.md +35 -0
  27. package/skills/finance/crypto-defi.md +39 -0
  28. package/skills/finance/financial-analyst.md +35 -0
  29. package/skills/healthcare/health-wellness.md +32 -0
  30. package/skills/healthcare/medical-researcher.md +33 -0
  31. package/skills/legal/contract-reviewer.md +35 -0
  32. package/skills/legal/legal-advisor.md +36 -0
  33. package/skills/marketing/content-marketer.md +38 -0
  34. package/skills/marketing/growth.md +38 -0
  35. package/skills/marketing/seo.md +43 -0
  36. package/skills/marketing/social-media.md +43 -0
  37. package/skills/writing/academic-writer.md +33 -0
  38. package/skills/writing/copywriter.md +32 -0
  39. package/skills/writing/creative-writer.md +32 -0
  40. package/skills/writing/tech-writer.md +33 -0
  41. package/src/agent.js +153 -118
  42. package/src/automation/scheduler.js +36 -3
  43. package/src/bot.js +147 -64
  44. package/src/coder.js +30 -8
  45. package/src/conversation.js +96 -19
  46. package/src/dashboard/dashboard.css +6 -0
  47. package/src/dashboard/dashboard.js +28 -1
  48. package/src/dashboard/index.html +12 -0
  49. package/src/dashboard/server.js +77 -15
  50. package/src/life/codebase.js +2 -1
  51. package/src/life/daydream_engine.js +386 -0
  52. package/src/life/engine.js +1 -0
  53. package/src/life/evolution.js +4 -3
  54. package/src/prompts/orchestrator.js +1 -1
  55. package/src/prompts/system.js +1 -1
  56. package/src/prompts/workers.js +8 -1
  57. package/src/providers/anthropic.js +3 -1
  58. package/src/providers/base.js +33 -0
  59. package/src/providers/index.js +1 -1
  60. package/src/providers/models.js +22 -0
  61. package/src/providers/openai-compat.js +3 -0
  62. package/src/services/x-api.js +14 -3
  63. package/src/skills/loader.js +382 -0
  64. package/src/swarm/worker-registry.js +2 -2
  65. package/src/tools/browser.js +10 -3
  66. package/src/tools/coding.js +16 -0
  67. package/src/tools/docker.js +13 -0
  68. package/src/tools/git.js +31 -29
  69. package/src/tools/jira.js +11 -2
  70. package/src/tools/monitor.js +9 -1
  71. package/src/tools/network.js +34 -0
  72. package/src/tools/orchestrator-tools.js +2 -1
  73. package/src/tools/os.js +20 -6
  74. package/src/utils/config.js +1 -1
  75. package/src/utils/display.js +1 -1
  76. package/src/utils/logger.js +1 -1
  77. package/src/utils/timeAwareness.js +72 -0
  78. package/src/worker.js +26 -33
  79. package/src/skills/catalog.js +0 -506
  80. package/src/skills/custom.js +0 -128
package/src/agent.js CHANGED
@@ -1,10 +1,11 @@
1
- import { createProvider, PROVIDERS } from './providers/index.js';
1
+ import { createProvider, PROVIDERS, MODEL_FALLBACKS } from './providers/index.js';
2
+ import { BaseProvider } from './providers/base.js';
2
3
  import { orchestratorToolDefinitions, executeOrchestratorTool } from './tools/orchestrator-tools.js';
3
4
  import { getToolsForWorker } from './swarm/worker-registry.js';
4
5
  import { WORKER_TYPES } from './swarm/worker-registry.js';
5
6
  import { getOrchestratorPrompt } from './prompts/orchestrator.js';
6
7
  import { getWorkerPrompt } from './prompts/workers.js';
7
- import { getUnifiedSkillById } from './skills/custom.js';
8
+ import { getSkillById, buildSkillPrompt, filterSkillsForWorker } from './skills/loader.js';
8
9
  import { WorkerAgent } from './worker.js';
9
10
  import { getLogger } from './utils/logger.js';
10
11
  import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestratorToYaml, saveClaudeCodeModelToYaml, saveClaudeCodeAuth } from './utils/config.js';
@@ -79,8 +80,8 @@ export class OrchestratorAgent {
79
80
  _getSystemPrompt(chatId, user, temporalContext = null) {
80
81
  const logger = getLogger();
81
82
  const key = this._chatKey(chatId);
82
- const skillId = this.conversationManager.getSkill(key);
83
- const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
83
+ const skillIds = this.conversationManager.getSkills(key);
84
+ const skillPrompt = buildSkillPrompt(skillIds);
84
85
 
85
86
  let userPersona = null;
86
87
  if (this.personaManager && user?.id) {
@@ -104,21 +105,47 @@ export class OrchestratorAgent {
104
105
  sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
105
106
  }
106
107
 
107
- logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'} | character=${this._activeCharacterId || 'default'}`);
108
+ logger.debug(`Orchestrator building system prompt for chat ${chatId} | skills=${skillIds.length > 0 ? skillIds.join(',') : 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'} | character=${this._activeCharacterId || 'default'}`);
108
109
  return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext, this._activePersonaMd, this._activeCharacterName);
109
110
  }
110
111
 
112
+ // ── Multi-skill methods ─────────────────────────────────────────────
113
+
114
+ /** Toggle a skill on/off for a chat. Returns { added: bool, skills: string[] }. */
115
+ toggleSkill(chatId, skillId) {
116
+ const key = this._chatKey(chatId);
117
+ const current = this.conversationManager.getSkills(key);
118
+ if (current.includes(skillId)) {
119
+ this.conversationManager.removeSkill(key, skillId);
120
+ return { added: false, skills: this.conversationManager.getSkills(key) };
121
+ }
122
+ const added = this.conversationManager.addSkill(key, skillId);
123
+ return { added, skills: this.conversationManager.getSkills(key) };
124
+ }
125
+
126
+ /** Get all active skill IDs for a chat. */
127
+ getActiveSkillIds(chatId) {
128
+ return this.conversationManager.getSkills(this._chatKey(chatId));
129
+ }
130
+
131
+ /** Get all active skill objects for a chat. */
132
+ getActiveSkills(chatId) {
133
+ const ids = this.getActiveSkillIds(chatId);
134
+ return ids.map(id => getSkillById(id)).filter(Boolean);
135
+ }
136
+
137
+ // Backward-compat aliases
111
138
  setSkill(chatId, skillId) {
112
139
  this.conversationManager.setSkill(this._chatKey(chatId), skillId);
113
140
  }
114
141
 
115
142
  clearSkill(chatId) {
116
- this.conversationManager.clearSkill(this._chatKey(chatId));
143
+ this.conversationManager.clearSkills(this._chatKey(chatId));
117
144
  }
118
145
 
119
146
  getActiveSkill(chatId) {
120
- const skillId = this.conversationManager.getSkill(this._chatKey(chatId));
121
- return skillId ? getUnifiedSkillById(skillId) : null;
147
+ const skills = this.getActiveSkills(chatId);
148
+ return skills.length > 0 ? skills[0] : null;
122
149
  }
123
150
 
124
151
  /**
@@ -508,44 +535,52 @@ export class OrchestratorAgent {
508
535
 
509
536
  logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars, structured: ${!!job.structuredResult}`);
510
537
 
511
- // 1. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
512
- const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
513
- logger.debug(`[Orchestrator] Job ${job.id} notification sent — msgId=${notifyMsgId || 'none'}`);
538
+ // 1. Store the job result in conversation history so the Orchestrator has full context
539
+ const resultEntry = this._buildResultHistoryEntry(job);
540
+ this.conversationManager.addMessage(convKey, 'assistant', resultEntry);
514
541
 
515
- // 2. Try to summarize, then store ONE message in history (summary or fallback — not both)
542
+ // 2. Trigger a proactive Orchestrator generation inject the job result as context
543
+ // and let the Orchestrator speak naturally (present results, suggest next steps, etc.)
516
544
  try {
517
- const summary = await this._summarizeJobResult(chatId, job);
518
- if (summary) {
519
- logger.debug(`[Orchestrator] Job ${job.id} summary ready (${summary.length} chars) — delivering to user`);
520
- this.conversationManager.addMessage(convKey, 'assistant', summary);
521
- // Try to edit the notification, fall back to new message if edit fails
522
- try {
523
- await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
524
- } catch {
525
- await this._sendUpdate(chatId, summary).catch(() => {});
545
+ const sr = job.structuredResult;
546
+ let resultContext;
547
+ if (sr?.structured) {
548
+ const parts = [`Summary: ${sr.summary}`, `Status: ${sr.status}`];
549
+ if (sr.artifacts?.length > 0) {
550
+ parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path || ''}`).join(', ')}`);
526
551
  }
527
- } else {
528
- // Summary was null — store the fallback
529
- const fallback = this._buildSummaryFallback(job, label);
530
- logger.debug(`[Orchestrator] Job ${job.id} using fallback (${fallback.length} chars) — delivering to user`);
531
- this.conversationManager.addMessage(convKey, 'assistant', fallback);
532
- try {
533
- await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId });
534
- } catch {
535
- await this._sendUpdate(chatId, fallback).catch(() => {});
552
+ if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
553
+ if (sr.details) {
554
+ const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
555
+ parts.push(`Details:\n${d.slice(0, 4000)}`);
536
556
  }
557
+ resultContext = parts.join('\n');
558
+ } else {
559
+ resultContext = (job.result || 'Done.').slice(0, 4000);
560
+ }
561
+
562
+ const triggerMessage = `[System: The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s). Results:\n\n${resultContext}\n\nProactively notify the user about the completed task. Present the results naturally, celebrate if appropriate, and suggest next steps. Do NOT mention "worker" or internal job details — speak as if you did the work yourself.]`;
563
+
564
+ // Add trigger as a transient user message (not stored in persistent history)
565
+ const messages = [...this.conversationManager.getSummarizedHistory(convKey)];
566
+ messages.push({ role: 'user', content: triggerMessage });
567
+
568
+ logger.info(`[Orchestrator] Triggering proactive follow-up for job ${job.id} in chat ${chatId}`);
569
+
570
+ const { max_tool_depth } = this.config.orchestrator;
571
+ const reply = await this._runLoop(chatId, messages, null, 0, max_tool_depth);
572
+
573
+ if (reply) {
574
+ logger.info(`[Orchestrator] Proactive reply for job ${job.id}: "${reply.slice(0, 200)}"`);
575
+ // _runLoop already stores the reply in conversation history
576
+ await this._sendUpdate(chatId, reply);
537
577
  }
538
578
  } catch (err) {
539
- logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
540
- // Store the fallback so the orchestrator retains context about what happened
579
+ logger.error(`[Orchestrator] Proactive follow-up failed for job ${job.id}: ${err.message}`);
580
+ // Fallback: send a simple formatted summary so the user isn't left hanging
541
581
  const fallback = this._buildSummaryFallback(job, label);
542
582
  this.conversationManager.addMessage(convKey, 'assistant', fallback);
543
- // Try to edit notification, fall back to new message
544
- try {
545
- await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId });
546
- } catch {
547
- await this._sendUpdate(chatId, fallback).catch(() => {});
548
- }
583
+ await this._sendUpdate(chatId, fallback).catch(() => {});
549
584
  }
550
585
  });
551
586
 
@@ -564,7 +599,7 @@ export class OrchestratorAgent {
564
599
  }
565
600
  });
566
601
 
567
- this.jobManager.on('job:failed', (job) => {
602
+ this.jobManager.on('job:failed', async (job) => {
568
603
  const chatId = job.chatId;
569
604
  const convKey = this._chatKey(chatId);
570
605
  const workerDef = WORKER_TYPES[job.workerType] || {};
@@ -572,9 +607,32 @@ export class OrchestratorAgent {
572
607
 
573
608
  logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
574
609
 
575
- const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
576
- this.conversationManager.addMessage(convKey, 'assistant', msg);
577
- this._sendUpdate(chatId, msg).catch(() => {});
610
+ // Store failure context in history
611
+ const failMsg = `[${label} failed — job ${job.id}]: ${job.error}`;
612
+ this.conversationManager.addMessage(convKey, 'assistant', failMsg);
613
+
614
+ // Trigger proactive Orchestrator follow-up for the failure
615
+ try {
616
+ const triggerMessage = `[System: The ${label} worker failed on job \`${job.id}\`. Error: ${job.error}\n\nProactively notify the user about the failure. Explain what went wrong in simple terms, apologize if appropriate, and suggest how to fix or retry. Do NOT mention "worker" or internal job details — speak as if you encountered the issue yourself.]`;
617
+
618
+ const messages = [...this.conversationManager.getSummarizedHistory(convKey)];
619
+ messages.push({ role: 'user', content: triggerMessage });
620
+
621
+ logger.info(`[Orchestrator] Triggering proactive follow-up for failed job ${job.id} in chat ${chatId}`);
622
+
623
+ const { max_tool_depth } = this.config.orchestrator;
624
+ const reply = await this._runLoop(chatId, messages, null, 0, max_tool_depth);
625
+
626
+ if (reply) {
627
+ logger.info(`[Orchestrator] Proactive failure reply for job ${job.id}: "${reply.slice(0, 200)}"`);
628
+ await this._sendUpdate(chatId, reply);
629
+ }
630
+ } catch (err) {
631
+ logger.error(`[Orchestrator] Proactive failure follow-up failed for job ${job.id}: ${err.message}`);
632
+ // Fallback: send the simple failure message
633
+ const fallback = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
634
+ await this._sendUpdate(chatId, fallback).catch(() => {});
635
+ }
578
636
  });
579
637
 
580
638
  this.jobManager.on('job:cancelled', (job) => {
@@ -589,73 +647,6 @@ export class OrchestratorAgent {
589
647
  });
590
648
  }
591
649
 
592
- /**
593
- * Auto-summarize a completed job result via the orchestrator LLM.
594
- * Uses structured data for focused summarization when available.
595
- * Short results (<500 chars) skip the LLM call entirely.
596
- * Protected by the provider's built-in timeout (30s).
597
- * Returns the summary text, or null. Caller handles delivery.
598
- */
599
- async _summarizeJobResult(chatId, job) {
600
- const logger = getLogger();
601
- const workerDef = WORKER_TYPES[job.workerType] || {};
602
- const label = workerDef.label || job.workerType;
603
-
604
- logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
605
-
606
- const sr = job.structuredResult;
607
- const resultLen = (job.result || '').length;
608
-
609
- // Direct coding jobs: Claude Code already produces clean, human-readable output.
610
- // Skip LLM summarization to avoid timeouts and latency — use the result directly.
611
- if (job.workerType === 'coding') {
612
- logger.info(`[Orchestrator] Job ${job.id} is a coding job — using direct result (no LLM summary)`);
613
- return this._buildSummaryFallback(job, label);
614
- }
615
-
616
- // Short results don't need LLM summarization
617
- if (sr?.structured && resultLen < 500) {
618
- logger.info(`[Orchestrator] Job ${job.id} result short enough — skipping LLM summary`);
619
- return this._buildSummaryFallback(job, label);
620
- }
621
-
622
- // Build a focused prompt using structured data if available
623
- let resultContext;
624
- if (sr?.structured) {
625
- const parts = [`Summary: ${sr.summary}`, `Status: ${sr.status}`];
626
- if (sr.artifacts?.length > 0) {
627
- parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path}`).join(', ')}`);
628
- }
629
- if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
630
- // Include details up to 8000 chars
631
- if (sr.details) {
632
- const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
633
- parts.push(`Details:\n${d.slice(0, 8000)}`);
634
- }
635
- resultContext = parts.join('\n');
636
- } else {
637
- resultContext = (job.result || 'Done.').slice(0, 8000);
638
- }
639
-
640
- const history = this.conversationManager.getSummarizedHistory(this._chatKey(chatId));
641
-
642
- const response = await this.orchestratorProvider.chat({
643
- system: this._getSystemPrompt(chatId, null),
644
- messages: [
645
- ...history,
646
- {
647
- role: 'user',
648
- content: `The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s). Here are the results:\n\n${resultContext}\n\nPresent these results to the user in a clean, well-formatted way. Don't mention "worker" or technical job details — just present the findings naturally as if you did the work yourself.`,
649
- },
650
- ],
651
- });
652
-
653
- const summary = response.text || '';
654
- logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
655
-
656
- return summary || null;
657
- }
658
-
659
650
  /**
660
651
  * Build a compact history entry for a completed job result.
661
652
  * Stored as role: 'assistant' (not fake 'user') with up to 6000 chars of detail.
@@ -862,9 +853,10 @@ export class OrchestratorAgent {
862
853
  }
863
854
  };
864
855
 
865
- // Get scoped tools and skill
856
+ // Get scoped tools and skills (filtered by worker affinity)
866
857
  const tools = getToolsForWorker(job.workerType);
867
- const skillId = this.conversationManager.getSkill(this._chatKey(chatId));
858
+ const allSkillIds = this.conversationManager.getSkills(this._chatKey(chatId));
859
+ const workerSkillIds = filterSkillsForWorker(allSkillIds, job.workerType);
868
860
 
869
861
  // Build worker context (conversation history, persona, dependency results)
870
862
  const workerContext = this._buildWorkerContext(job);
@@ -880,14 +872,14 @@ export class OrchestratorAgent {
880
872
  }
881
873
  }
882
874
 
883
- logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skill=${skillId || 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}, context=${workerContext ? 'yes' : 'none'}`);
875
+ logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skills=${workerSkillIds.length > 0 ? workerSkillIds.join(',') : 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}, context=${workerContext ? 'yes' : 'none'}`);
884
876
 
885
877
  const worker = new WorkerAgent({
886
878
  config: workerConfig,
887
879
  workerType: job.workerType,
888
880
  jobId: job.id,
889
881
  tools,
890
- skillId,
882
+ skillIds: workerSkillIds,
891
883
  workerContext,
892
884
  callbacks: {
893
885
  onProgress: (text) => addActivity(text),
@@ -1129,11 +1121,54 @@ export class OrchestratorAgent {
1129
1121
  workingMessages = [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...workingMessages];
1130
1122
  }
1131
1123
 
1132
- const response = await this.orchestratorProvider.chat({
1133
- system: this._getSystemPrompt(chatId, user, temporalContext),
1134
- messages: workingMessages,
1135
- tools: orchestratorToolDefinitions,
1136
- });
1124
+ let response;
1125
+ let activeProvider = this.orchestratorProvider;
1126
+
1127
+ try {
1128
+ response = await activeProvider.chat({
1129
+ system: this._getSystemPrompt(chatId, user, temporalContext),
1130
+ messages: workingMessages,
1131
+ tools: orchestratorToolDefinitions,
1132
+ });
1133
+ } catch (err) {
1134
+ // If this is a model limitation, try falling back to a simpler model
1135
+ const currentModel = activeProvider.model;
1136
+ const fallbackModel = MODEL_FALLBACKS[currentModel];
1137
+
1138
+ if (fallbackModel && BaseProvider.isModelLimitation(err)) {
1139
+ logger.warn(`[Orchestrator] Model ${currentModel} hit limitation: ${err.message} — falling back to ${fallbackModel}`);
1140
+ this._sendUpdate(chatId, `⚠️ ${currentModel} hit a limitation, switching to ${fallbackModel}...`).catch(() => {});
1141
+
1142
+ try {
1143
+ const orchProviderKey = this.config.orchestrator.provider || 'anthropic';
1144
+ const orchProviderDef = PROVIDERS[orchProviderKey];
1145
+ const orchApiKey = this.config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]);
1146
+
1147
+ const fallbackProvider = createProvider({
1148
+ brain: {
1149
+ provider: orchProviderKey,
1150
+ model: fallbackModel,
1151
+ max_tokens: this.config.orchestrator.max_tokens,
1152
+ temperature: this.config.orchestrator.temperature,
1153
+ api_key: orchApiKey,
1154
+ timeout: 30_000,
1155
+ },
1156
+ });
1157
+
1158
+ response = await fallbackProvider.chat({
1159
+ system: this._getSystemPrompt(chatId, user, temporalContext),
1160
+ messages: workingMessages,
1161
+ tools: orchestratorToolDefinitions,
1162
+ });
1163
+ activeProvider = fallbackProvider;
1164
+ } catch (fallbackErr) {
1165
+ logger.error(`[Orchestrator] Fallback model ${fallbackModel} also failed: ${fallbackErr.message}`);
1166
+ throw err; // Throw the original error
1167
+ }
1168
+ } else {
1169
+ throw err;
1170
+ }
1171
+ }
1137
1172
 
1138
1173
  logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
1139
1174
 
@@ -108,7 +108,13 @@ function nextCronTime(expression, after) {
108
108
  }
109
109
 
110
110
  /**
111
- * Parse a single cron field into a Set of valid values.
111
+ * Parse a single cron field into a Set of valid integer values.
112
+ *
113
+ * @param {string} field - Cron field expression (e.g. '*', '1-5', '*\/10', '1,3,5')
114
+ * @param {number} min - Minimum allowed value for this field (inclusive)
115
+ * @param {number} max - Maximum allowed value for this field (inclusive)
116
+ * @returns {Set<number>} Set of matching integer values clamped to [min, max]
117
+ * @throws {Error} If the field contains invalid syntax or out-of-range values
112
118
  */
113
119
  function parseField(field, min, max) {
114
120
  const values = new Set();
@@ -119,21 +125,48 @@ function parseField(field, min, max) {
119
125
  } else if (part.includes('/')) {
120
126
  const [range, stepStr] = part.split('/');
121
127
  const step = parseInt(stepStr, 10);
128
+ if (isNaN(step) || step <= 0) {
129
+ throw new Error(`Invalid cron step value "${stepStr}" in field "${field}"`);
130
+ }
122
131
  let start = min;
123
132
  let end = max;
124
133
  if (range !== '*') {
125
134
  if (range.includes('-')) {
126
- [start, end] = range.split('-').map(Number);
135
+ const parts = range.split('-').map(Number);
136
+ start = parts[0];
137
+ end = parts[1];
138
+ if (isNaN(start) || isNaN(end)) {
139
+ throw new Error(`Invalid cron range "${range}" in field "${field}"`);
140
+ }
127
141
  } else {
128
142
  start = parseInt(range, 10);
143
+ if (isNaN(start)) {
144
+ throw new Error(`Invalid cron value "${range}" in field "${field}"`);
145
+ }
129
146
  }
130
147
  }
148
+ if (start < min || end > max) {
149
+ throw new Error(`Cron field value out of range [${min}-${max}] in "${field}"`);
150
+ }
131
151
  for (let i = start; i <= end; i += step) values.add(i);
132
152
  } else if (part.includes('-')) {
133
153
  const [s, e] = part.split('-').map(Number);
154
+ if (isNaN(s) || isNaN(e)) {
155
+ throw new Error(`Invalid cron range "${part}" in field "${field}"`);
156
+ }
157
+ if (s < min || e > max) {
158
+ throw new Error(`Cron field value out of range [${min}-${max}] in "${field}"`);
159
+ }
134
160
  for (let i = s; i <= e; i++) values.add(i);
135
161
  } else {
136
- values.add(parseInt(part, 10));
162
+ const val = parseInt(part, 10);
163
+ if (isNaN(val)) {
164
+ throw new Error(`Invalid cron value "${part}" in field "${field}"`);
165
+ }
166
+ if (val < min || val > max) {
167
+ throw new Error(`Cron field value ${val} out of range [${min}-${max}] in "${field}"`);
168
+ }
169
+ values.add(val);
137
170
  }
138
171
  }
139
172