@defai.digital/automatosx 6.2.1 → 6.2.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [6.2.5] - 2025-10-31
6
+
7
+ ### 🔧 Fixes
8
+
9
+ **Routing and Cost Estimation Bugs (Bugs #31, #32)**
10
+
11
+ Through heavy-thinking analysis and ax agent collaboration, identified and fixed 2 critical bugs in routing systems:
12
+
13
+ - **Bug #31 (MEDIUM)**: Division by zero in routing statistics (`src/core/routing-strategy.ts:317`)
14
+ - **Problem**: Empty scores array caused `0/0 = NaN` in avgScores calculation
15
+ - **Impact**: Corrupted routing statistics with NaN values
16
+ - **Fix**: Added guard to check `scores.length === 0` before division, return 0 for empty arrays
17
+ - **Pattern**: Same defensive approach as Bugs #16, #19
18
+
19
+ - **Bug #32 (HIGH)**: Unsafe metadata access crashes cost estimation (`src/core/router.ts:878-880`)
20
+ - **Problem**: Accessed `metadata.costPerToken.input/output` without validation
21
+ - **Impact**: TypeError crash if provider metadata missing `costPerToken` field
22
+ - **Fix**: Added comprehensive validation of metadata structure before access
23
+ - **Fallback**: Uses fallback pricing ($2.50/$10.00 per 1M tokens) if metadata invalid
24
+ - **Pattern**: Defensive programming to prevent production crashes
25
+
26
+ ### 📊 Impact
27
+
28
+ - **Users affected**: All users using multi-factor routing and cost tracking
29
+ - **Breaking changes**: None
30
+ - **Migration**: None required - automatic graceful fallback for missing metadata
31
+
32
+ ### ✅ Testing
33
+
34
+ - All 2,281 unit tests passing
35
+ - TypeScript compilation successful
36
+ - Build successful
37
+
5
38
  ## [6.0.7] - 2025-10-30
6
39
 
7
40
  ### 🔧 Fixes
package/README.md CHANGED
@@ -7,7 +7,7 @@ AutomatosX is the only AI CLI that combines declarative workflow specs, policy-d
7
7
  [![npm version](https://img.shields.io/npm/v/@defai.digital/automatosx.svg)](https://www.npmjs.com/package/@defai.digital/automatosx)
8
8
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue.svg)](https://www.typescriptlang.org/)
10
- [![Tests](https://img.shields.io/badge/tests-2,457%20passing-brightgreen.svg)](#)
10
+ [![Tests](https://img.shields.io/badge/tests-2,468%20passing-brightgreen.svg)](#)
11
11
  [![macOS](https://img.shields.io/badge/macOS-26.0-blue.svg)](https://www.apple.com/macos)
12
12
  [![Windows](https://img.shields.io/badge/Windows-10+-blue.svg)](https://www.microsoft.com/windows)
13
13
  [![Ubuntu](https://img.shields.io/badge/Ubuntu-24.04-orange.svg)](https://ubuntu.com)
package/dist/index.js CHANGED
@@ -6364,12 +6364,16 @@ var init_openai_sdk_provider = __esm({
6364
6364
  init_provider_connection_pool();
6365
6365
  init_provider_limit_manager();
6366
6366
  init_streaming_feedback();
6367
+ init_timeout_estimator();
6367
6368
  OpenAISDKProvider = class extends BaseProvider {
6368
6369
  sdkConfig;
6369
6370
  connectionPool = getProviderConnectionPool();
6370
6371
  initialized = false;
6371
6372
  // v6.0.7: Streaming feedback
6372
6373
  currentStreamingFeedback = null;
6374
+ // v6.2.2: Progress tracking for SDK streaming (bugfix #5, #6)
6375
+ currentProgressTracker = null;
6376
+ progressInterval = null;
6373
6377
  constructor(config, sdkConfig = {}) {
6374
6378
  super(config);
6375
6379
  this.sdkConfig = sdkConfig;
@@ -6437,10 +6441,25 @@ var init_openai_sdk_provider = __esm({
6437
6441
  */
6438
6442
  async executeRequest(request) {
6439
6443
  const startTime = Date.now();
6444
+ const fullPrompt = `${request.systemPrompt || ""}
6445
+ ${request.prompt}`.trim();
6446
+ const timeoutEstimate = estimateTimeout({
6447
+ prompt: fullPrompt,
6448
+ systemPrompt: request.systemPrompt,
6449
+ model: typeof request.model === "string" ? request.model : void 0,
6450
+ maxTokens: request.maxTokens
6451
+ });
6452
+ if (process.env.AUTOMATOSX_QUIET !== "true") {
6453
+ logger.info(formatTimeoutEstimate(timeoutEstimate));
6454
+ }
6455
+ if (timeoutEstimate.estimatedDurationMs > 1e4) {
6456
+ this.startProgressTracking(timeoutEstimate.estimatedDurationMs);
6457
+ }
6440
6458
  const useMock = process.env.AUTOMATOSX_MOCK_PROVIDERS === "true" || process.env.NODE_ENV === "test" || process.env.VITEST === "true";
6441
6459
  if (useMock) {
6442
6460
  const mockPrompt = request.prompt.substring(0, 100);
6443
6461
  const latency = Date.now() - startTime;
6462
+ this.stopProgressTracking();
6444
6463
  return {
6445
6464
  content: `[Mock Response from OpenAI SDK]
6446
6465
 
@@ -6477,6 +6496,7 @@ This is a placeholder response. Set AUTOMATOSX_MOCK_PROVIDERS=false to use real
6477
6496
  // Non-streaming for this method
6478
6497
  });
6479
6498
  await this.connectionPool.release(this.config.name, connection);
6499
+ this.stopProgressTracking();
6480
6500
  const content = response.choices[0]?.message?.content || "";
6481
6501
  const finishReason = this.mapFinishReason(response.choices[0]?.finish_reason);
6482
6502
  const latency = Date.now() - startTime;
@@ -6500,9 +6520,11 @@ This is a placeholder response. Set AUTOMATOSX_MOCK_PROVIDERS=false to use real
6500
6520
  };
6501
6521
  } catch (error) {
6502
6522
  await this.connectionPool.release(this.config.name, connection);
6523
+ this.stopProgressTracking();
6503
6524
  throw error;
6504
6525
  }
6505
6526
  } catch (error) {
6527
+ this.stopProgressTracking();
6506
6528
  return this.handleSDKError(error, request, startTime);
6507
6529
  }
6508
6530
  }
@@ -6511,6 +6533,20 @@ This is a placeholder response. Set AUTOMATOSX_MOCK_PROVIDERS=false to use real
6511
6533
  */
6512
6534
  async executeStreaming(request, options) {
6513
6535
  const startTime = Date.now();
6536
+ const fullPrompt = `${request.systemPrompt || ""}
6537
+ ${request.prompt}`.trim();
6538
+ const timeoutEstimate = estimateTimeout({
6539
+ prompt: fullPrompt,
6540
+ systemPrompt: request.systemPrompt,
6541
+ model: typeof request.model === "string" ? request.model : void 0,
6542
+ maxTokens: request.maxTokens
6543
+ });
6544
+ if (process.env.AUTOMATOSX_QUIET !== "true") {
6545
+ logger.info(formatTimeoutEstimate(timeoutEstimate));
6546
+ }
6547
+ if (timeoutEstimate.estimatedDurationMs > 1e4) {
6548
+ this.startProgressTracking(timeoutEstimate.estimatedDurationMs);
6549
+ }
6514
6550
  const estimatedOutputTokens = request.maxTokens || this.estimateTokens(request.prompt) * 2;
6515
6551
  this.currentStreamingFeedback = createStreamingFeedback(estimatedOutputTokens);
6516
6552
  const useMock = process.env.AUTOMATOSX_MOCK_PROVIDERS === "true" || process.env.NODE_ENV === "test" || process.env.VITEST === "true";
@@ -6531,6 +6567,7 @@ This is a placeholder streaming response. Set AUTOMATOSX_MOCK_PROVIDERS=false to
6531
6567
  this.currentStreamingFeedback.stop(50);
6532
6568
  this.currentStreamingFeedback = null;
6533
6569
  }
6570
+ this.stopProgressTracking();
6534
6571
  return {
6535
6572
  content: mockContent,
6536
6573
  model: request.model || "gpt-4o",
@@ -6586,6 +6623,19 @@ This is a placeholder streaming response. Set AUTOMATOSX_MOCK_PROVIDERS=false to
6586
6623
  });
6587
6624
  }
6588
6625
  }
6626
+ if (options.onProgress) {
6627
+ try {
6628
+ const currentTokens = this.estimateTokens(fullContent);
6629
+ const expectedTokens = request.maxTokens || 4096;
6630
+ const progress = Math.min(currentTokens / expectedTokens, 0.95);
6631
+ options.onProgress(progress);
6632
+ } catch (error) {
6633
+ logger.warn("Streaming onProgress callback error", {
6634
+ provider: this.config.name,
6635
+ error: error instanceof Error ? error.message : String(error)
6636
+ });
6637
+ }
6638
+ }
6589
6639
  }
6590
6640
  if (chunk.choices[0]?.finish_reason) {
6591
6641
  finishReason = this.mapFinishReason(chunk.choices[0].finish_reason);
@@ -6616,6 +6666,17 @@ This is a placeholder streaming response. Set AUTOMATOSX_MOCK_PROVIDERS=false to
6616
6666
  this.currentStreamingFeedback.stop(completionTokens);
6617
6667
  this.currentStreamingFeedback = null;
6618
6668
  }
6669
+ if (options.onProgress) {
6670
+ try {
6671
+ options.onProgress(1);
6672
+ } catch (error) {
6673
+ logger.warn("Final streaming onProgress callback error", {
6674
+ provider: this.config.name,
6675
+ error: error instanceof Error ? error.message : String(error)
6676
+ });
6677
+ }
6678
+ }
6679
+ this.stopProgressTracking();
6619
6680
  return {
6620
6681
  content: fullContent,
6621
6682
  model: modelUsed,
@@ -6633,6 +6694,7 @@ This is a placeholder streaming response. Set AUTOMATOSX_MOCK_PROVIDERS=false to
6633
6694
  this.currentStreamingFeedback.stop();
6634
6695
  this.currentStreamingFeedback = null;
6635
6696
  }
6697
+ this.stopProgressTracking();
6636
6698
  throw error;
6637
6699
  }
6638
6700
  } catch (error) {
@@ -6640,6 +6702,7 @@ This is a placeholder streaming response. Set AUTOMATOSX_MOCK_PROVIDERS=false to
6640
6702
  this.currentStreamingFeedback.stop();
6641
6703
  this.currentStreamingFeedback = null;
6642
6704
  }
6705
+ this.stopProgressTracking();
6643
6706
  return this.handleSDKError(error, request, startTime);
6644
6707
  }
6645
6708
  }
@@ -6808,6 +6871,38 @@ This is a placeholder streaming response. Set AUTOMATOSX_MOCK_PROVIDERS=false to
6808
6871
  tokensUsed: inputTokens + outputTokens
6809
6872
  };
6810
6873
  }
6874
+ /**
6875
+ * Start progress tracking for long operations
6876
+ * v6.2.2: Consistency with CLI providers (bugfix #5, #6)
6877
+ * @param estimatedDurationMs - Estimated duration in milliseconds
6878
+ */
6879
+ startProgressTracking(estimatedDurationMs) {
6880
+ if (process.env.AUTOMATOSX_QUIET === "true") {
6881
+ return;
6882
+ }
6883
+ this.currentProgressTracker = new ProgressTracker(estimatedDurationMs);
6884
+ this.progressInterval = setInterval(() => {
6885
+ if (this.currentProgressTracker && this.currentProgressTracker.shouldUpdate()) {
6886
+ process.stderr.write("\r" + this.currentProgressTracker.formatProgress());
6887
+ }
6888
+ }, 1e3);
6889
+ }
6890
+ /**
6891
+ * Stop progress tracking
6892
+ * v6.2.2: Consistency with CLI providers (bugfix #5, #6)
6893
+ */
6894
+ stopProgressTracking() {
6895
+ if (this.progressInterval) {
6896
+ clearInterval(this.progressInterval);
6897
+ this.progressInterval = null;
6898
+ }
6899
+ if (this.currentProgressTracker) {
6900
+ if (process.env.AUTOMATOSX_QUIET !== "true") {
6901
+ process.stderr.write("\r" + " ".repeat(80) + "\r");
6902
+ }
6903
+ this.currentProgressTracker = null;
6904
+ }
6905
+ }
6811
6906
  };
6812
6907
  }
6813
6908
  });
@@ -11702,6 +11797,30 @@ var ProviderMetricsTracker = class extends EventEmitter {
11702
11797
  * Record a request for metrics tracking
11703
11798
  */
11704
11799
  async recordRequest(provider, latencyMs, success, finishReason, tokenUsage, costUsd, model) {
11800
+ if (!provider || provider.trim().length === 0) {
11801
+ throw new Error("Provider name cannot be empty");
11802
+ }
11803
+ if (!Number.isFinite(latencyMs)) {
11804
+ throw new Error(`Invalid latencyMs value: ${latencyMs}. Must be a finite number.`);
11805
+ }
11806
+ if (latencyMs < 0) {
11807
+ throw new Error(`Invalid latencyMs value: ${latencyMs}. Cannot be negative.`);
11808
+ }
11809
+ if (!Number.isFinite(costUsd)) {
11810
+ throw new Error(`Invalid costUsd value: ${costUsd}. Must be a finite number.`);
11811
+ }
11812
+ if (costUsd < 0) {
11813
+ throw new Error(`Invalid costUsd value: ${costUsd}. Cannot be negative.`);
11814
+ }
11815
+ if (!Number.isFinite(tokenUsage.prompt) || tokenUsage.prompt < 0) {
11816
+ throw new Error(`Invalid prompt tokens: ${tokenUsage.prompt}. Must be a non-negative finite number.`);
11817
+ }
11818
+ if (!Number.isFinite(tokenUsage.completion) || tokenUsage.completion < 0) {
11819
+ throw new Error(`Invalid completion tokens: ${tokenUsage.completion}. Must be a non-negative finite number.`);
11820
+ }
11821
+ if (!Number.isFinite(tokenUsage.total) || tokenUsage.total < 0) {
11822
+ throw new Error(`Invalid total tokens: ${tokenUsage.total}. Must be a non-negative finite number.`);
11823
+ }
11705
11824
  if (!this.metrics.has(provider)) {
11706
11825
  this.metrics.set(provider, []);
11707
11826
  }
@@ -12181,6 +12300,10 @@ var RoutingStrategyManager = class extends EventEmitter {
12181
12300
  }
12182
12301
  }
12183
12302
  for (const [provider, scores] of Object.entries(scoresByProvider)) {
12303
+ if (scores.length === 0) {
12304
+ stats.avgScores[provider] = 0;
12305
+ continue;
12306
+ }
12184
12307
  const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
12185
12308
  stats.avgScores[provider] = avg;
12186
12309
  }
@@ -12986,49 +13109,91 @@ var CostTracker = class extends EventEmitter {
12986
13109
  }
12987
13110
  /**
12988
13111
  * Initialize database
13112
+ * v6.2.4: Bug fix #24 - Wrap in try-catch to prevent memory leaks on error
12989
13113
  */
12990
13114
  async initialize() {
12991
13115
  if (this.initialized) {
12992
13116
  return;
12993
13117
  }
12994
- const dir = dirname(this.dbPath);
12995
- if (!existsSync(dir)) {
12996
- mkdirSync(dir, { recursive: true });
12997
- }
12998
- this.db = new Database3(this.dbPath);
12999
- this.db.pragma("journal_mode = WAL");
13000
- this.db.exec(`
13001
- CREATE TABLE IF NOT EXISTS cost_entries (
13002
- id INTEGER PRIMARY KEY AUTOINCREMENT,
13003
- timestamp INTEGER NOT NULL,
13004
- provider TEXT NOT NULL,
13005
- model TEXT NOT NULL,
13006
- session_id TEXT,
13007
- agent TEXT,
13008
- prompt_tokens INTEGER NOT NULL,
13009
- completion_tokens INTEGER NOT NULL,
13010
- total_tokens INTEGER NOT NULL,
13011
- estimated_cost_usd REAL NOT NULL,
13012
- request_id TEXT
13013
- );
13118
+ try {
13119
+ const dir = dirname(this.dbPath);
13120
+ if (!existsSync(dir)) {
13121
+ mkdirSync(dir, { recursive: true });
13122
+ }
13123
+ this.db = new Database3(this.dbPath);
13124
+ this.db.pragma("journal_mode = WAL");
13125
+ this.db.exec(`
13126
+ CREATE TABLE IF NOT EXISTS cost_entries (
13127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13128
+ timestamp INTEGER NOT NULL,
13129
+ provider TEXT NOT NULL,
13130
+ model TEXT NOT NULL,
13131
+ session_id TEXT,
13132
+ agent TEXT,
13133
+ prompt_tokens INTEGER NOT NULL,
13134
+ completion_tokens INTEGER NOT NULL,
13135
+ total_tokens INTEGER NOT NULL,
13136
+ estimated_cost_usd REAL NOT NULL,
13137
+ request_id TEXT
13138
+ );
13014
13139
 
13015
- CREATE INDEX IF NOT EXISTS idx_cost_timestamp ON cost_entries(timestamp);
13016
- CREATE INDEX IF NOT EXISTS idx_cost_provider ON cost_entries(provider);
13017
- CREATE INDEX IF NOT EXISTS idx_cost_session ON cost_entries(session_id);
13018
- CREATE INDEX IF NOT EXISTS idx_cost_agent ON cost_entries(agent);
13019
- `);
13020
- this.initialized = true;
13021
- logger.info("CostTracker initialized", {
13022
- dbPath: this.dbPath
13023
- });
13140
+ CREATE INDEX IF NOT EXISTS idx_cost_timestamp ON cost_entries(timestamp);
13141
+ CREATE INDEX IF NOT EXISTS idx_cost_provider ON cost_entries(provider);
13142
+ CREATE INDEX IF NOT EXISTS idx_cost_session ON cost_entries(session_id);
13143
+ CREATE INDEX IF NOT EXISTS idx_cost_agent ON cost_entries(agent);
13144
+ `);
13145
+ this.initialized = true;
13146
+ logger.info("CostTracker initialized", {
13147
+ dbPath: this.dbPath
13148
+ });
13149
+ } catch (error) {
13150
+ if (this.db) {
13151
+ try {
13152
+ this.db.close();
13153
+ } catch (closeError) {
13154
+ }
13155
+ this.db = null;
13156
+ }
13157
+ this.initialized = false;
13158
+ logger.error("Failed to initialize CostTracker", {
13159
+ error: error instanceof Error ? error.message : String(error),
13160
+ dbPath: this.dbPath
13161
+ });
13162
+ throw error;
13163
+ }
13024
13164
  }
13025
13165
  /**
13026
13166
  * Record cost for a request
13167
+ * v6.2.4: Bug fix #25 - Added input validation
13027
13168
  */
13028
13169
  async recordCost(entry) {
13029
13170
  if (!this.initialized) {
13030
13171
  await this.initialize();
13031
13172
  }
13173
+ if (!entry.provider || entry.provider.trim().length === 0) {
13174
+ throw new Error("Provider name cannot be empty");
13175
+ }
13176
+ if (!entry.model || entry.model.trim().length === 0) {
13177
+ throw new Error("Model name cannot be empty");
13178
+ }
13179
+ if (!Number.isFinite(entry.estimatedCostUsd)) {
13180
+ throw new Error(`Invalid estimatedCostUsd value: ${entry.estimatedCostUsd}. Must be a finite number.`);
13181
+ }
13182
+ if (entry.estimatedCostUsd < 0) {
13183
+ throw new Error(`Invalid estimatedCostUsd value: ${entry.estimatedCostUsd}. Cannot be negative.`);
13184
+ }
13185
+ if (!Number.isFinite(entry.promptTokens) || entry.promptTokens < 0) {
13186
+ throw new Error(`Invalid promptTokens: ${entry.promptTokens}. Must be a non-negative finite number.`);
13187
+ }
13188
+ if (!Number.isFinite(entry.completionTokens) || entry.completionTokens < 0) {
13189
+ throw new Error(`Invalid completionTokens: ${entry.completionTokens}. Must be a non-negative finite number.`);
13190
+ }
13191
+ if (!Number.isFinite(entry.totalTokens) || entry.totalTokens < 0) {
13192
+ throw new Error(`Invalid totalTokens: ${entry.totalTokens}. Must be a non-negative finite number.`);
13193
+ }
13194
+ if (!Number.isFinite(entry.timestamp) || entry.timestamp <= 0) {
13195
+ throw new Error(`Invalid timestamp: ${entry.timestamp}. Must be a positive finite number.`);
13196
+ }
13032
13197
  const stmt = this.db.prepare(`
13033
13198
  INSERT INTO cost_entries (
13034
13199
  timestamp, provider, model, session_id, agent,
@@ -14186,8 +14351,8 @@ var Router = class {
14186
14351
  */
14187
14352
  estimateCost(providerName, tokensUsed) {
14188
14353
  const metadata = PROVIDER_METADATA[providerName];
14189
- if (!metadata) {
14190
- logger.warn(`Provider metadata not found for cost estimation: ${providerName}, using fallback costs`);
14354
+ if (!metadata || !metadata.costPerToken || typeof metadata.costPerToken.input !== "number" || typeof metadata.costPerToken.output !== "number") {
14355
+ logger.warn(`Provider metadata or cost data not found for: ${providerName}, using fallback costs`);
14191
14356
  const inputCostPer1M = 2.5;
14192
14357
  const outputCostPer1M = 10;
14193
14358
  const inputCost2 = tokensUsed.prompt / 1e6 * inputCostPer1M;
@@ -14283,6 +14448,7 @@ var MemoryManager = class _MemoryManager {
14283
14448
  * Initialize database and load FTS5 extension
14284
14449
  *
14285
14450
  * v4.11.0: Added FTS5 full-text search support
14451
+ * v6.2.4: Bug fix #30 - Wrap in try-catch to prevent memory leaks on error
14286
14452
  */
14287
14453
  async initialize() {
14288
14454
  if (this.initialized) return;
@@ -14380,6 +14546,15 @@ var MemoryManager = class _MemoryManager {
14380
14546
  entryCount: this.entryCount
14381
14547
  });
14382
14548
  } catch (error) {
14549
+ if (this.db) {
14550
+ try {
14551
+ this.db.close();
14552
+ } catch (closeError) {
14553
+ }
14554
+ this.initialized = false;
14555
+ this.entryCount = 0;
14556
+ this.statements = {};
14557
+ }
14383
14558
  logger.error("Failed to initialize MemoryManager", { error: error.message });
14384
14559
  throw new MemoryError(
14385
14560
  `Failed to initialize memory system: ${error.message}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defai.digital/automatosx",
3
- "version": "6.2.1",
3
+ "version": "6.2.5",
4
4
  "description": "AI Agent Orchestration Platform",
5
5
  "type": "module",
6
6
  "publishConfig": {