@hamp10/agentforge 0.2.48 → 0.2.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.48",
3
+ "version": "0.2.50",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -549,6 +549,44 @@ try {
549
549
  /The task is:|Delete and rebuild both/i,
550
550
  'missing-target retries should not put the original reset-heavy task text at the top of the retry prompt'
551
551
  );
552
+ assert.throws(
553
+ () => cli._guardDirectFileWritePath(
554
+ path.join(fixture.repo, 'public_html', 'domains', 'alpha.html'),
555
+ fixture.repo,
556
+ { task: incompleteRepairLead }
557
+ ),
558
+ /focused retry pass/i,
559
+ 'direct writes should block already-touched scoped pages during a missing-target retry pass'
560
+ );
561
+ assert.doesNotThrow(
562
+ () => cli._guardDirectFileWritePath(
563
+ path.join(fixture.repo, 'public_html', 'domains', 'beta.html'),
564
+ fixture.repo,
565
+ { task: incompleteRepairLead }
566
+ ),
567
+ 'direct writes should allow the missing scoped page during a missing-target retry pass'
568
+ );
569
+ const visualRepairLead = worker._formatVisualRepairTaskLead(
570
+ 'Work on the Example.com listing pages for Alpha.ai and Beta.ai. Make both visually polished.',
571
+ 'beta-ai: Visual warning: heading is clipped.'
572
+ );
573
+ assert.throws(
574
+ () => cli._guardDirectFileWritePath(
575
+ path.join(fixture.repo, 'public_html', 'domains', 'alpha.html'),
576
+ fixture.repo,
577
+ { task: visualRepairLead }
578
+ ),
579
+ /focused retry pass/i,
580
+ 'direct writes should block non-failing scoped pages during a focused visual repair pass'
581
+ );
582
+ assert.doesNotThrow(
583
+ () => cli._guardDirectFileWritePath(
584
+ path.join(fixture.repo, 'public_html', 'domains', 'beta.html'),
585
+ fixture.repo,
586
+ { task: visualRepairLead }
587
+ ),
588
+ 'direct writes should allow the failing scoped page during a focused visual repair pass'
589
+ );
552
590
  assert.equal(
553
591
  worker._formatRepeatedVisualRepairNudge(2),
554
592
  '',
@@ -914,6 +914,24 @@ export class OpenClawCLI extends EventEmitter {
914
914
  return err;
915
915
  }
916
916
 
917
+ _directFocusedRetryAllowedSlugs(task) {
918
+ const text = String(task || '');
919
+ const patterns = [
920
+ /missing-target pass for target page\(s\) not yet changed in the current diff:\s*([^\n.]+)/i,
921
+ /visual repair pass for currently failing target page\(s\):\s*([^\n.]+)/i,
922
+ ];
923
+ for (const pattern of patterns) {
924
+ const match = text.match(pattern);
925
+ if (!match) continue;
926
+ const slugs = String(match[1] || '')
927
+ .split(/[,;]/)
928
+ .map(item => scopeSlug(item.trim()))
929
+ .filter(Boolean);
930
+ if (slugs.length > 0) return new Set(slugs);
931
+ }
932
+ return null;
933
+ }
934
+
917
935
  _isNestedProjectCopyPath(filePath, workDir) {
918
936
  const relative = path.relative(workDir, filePath);
919
937
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
@@ -1048,6 +1066,20 @@ export class OpenClawCLI extends EventEmitter {
1048
1066
  if (!this._directTaskScopeAllowsPath(filePath, workDir, options.task)) {
1049
1067
  throw this._directScopeViolationError(this._formatDirectScopeError(filePath, workDir, options.task));
1050
1068
  }
1069
+ const focusedRetrySlugs = this._directFocusedRetryAllowedSlugs(options.task);
1070
+ if (focusedRetrySlugs && slugs.length > 0) {
1071
+ const matchedScopedSlugs = this._directScopeSlugsMatchingText(lowerRelativePath, slugs);
1072
+ const matchesFocusedSlug = matchedScopedSlugs.some(slug => focusedRetrySlugs.has(slug));
1073
+ if (matchedScopedSlugs.length > 0 && !matchesFocusedSlug) {
1074
+ throw this._directScopeViolationError([
1075
+ 'Refusing to rewrite an already-addressed scoped target during a focused retry pass.',
1076
+ `Requested path: ${relativePath}`,
1077
+ `Current retry target(s): ${[...focusedRetrySlugs].join(', ')}.`,
1078
+ `Path appears to belong to: ${matchedScopedSlugs.join(', ')}.`,
1079
+ 'Continue from the current repo state and edit the focused target page(s) named by the retry instruction, then verify every requested target page locally.',
1080
+ ].join(' '));
1081
+ }
1082
+ }
1051
1083
  const collectionViolation = this._directNewHtmlPageCollectionViolation(
1052
1084
  filePath,
1053
1085
  workDir,
@@ -1788,7 +1820,7 @@ export class OpenClawCLI extends EventEmitter {
1788
1820
  if (!sessionFilePath || !existsSync(sessionFilePath)) return null;
1789
1821
  try {
1790
1822
  const lines = readFileSync(sessionFilePath, 'utf8').split('\n');
1791
- let input = 0, output = 0, costUsd = 0;
1823
+ let input = 0, output = 0, costUsd = 0, hasCost = false;
1792
1824
  for (const line of lines) {
1793
1825
  if (!line.trim()) continue;
1794
1826
  try {
@@ -1798,14 +1830,26 @@ export class OpenClawCLI extends EventEmitter {
1798
1830
  output += entry.message.usage.output || 0;
1799
1831
  // openclaw writes exact cost.total — use it directly instead of re-calculating
1800
1832
  if (entry.message.usage.cost?.total != null) {
1801
- costUsd += entry.message.usage.cost.total;
1833
+ const costTotal = Number(entry.message.usage.cost.total);
1834
+ if (Number.isFinite(costTotal) && costTotal >= 0) {
1835
+ costUsd += costTotal;
1836
+ hasCost = true;
1837
+ }
1802
1838
  }
1803
1839
  }
1804
1840
  } catch { /* skip malformed lines */ }
1805
1841
  }
1806
1842
  if (input > 0 || output > 0) {
1807
1843
  console.log(`[session-usage] Read from JSONL: input=${input} output=${output} cost=$${costUsd.toFixed(6)}`);
1808
- return { input_tokens: input, output_tokens: output, cost_usd: costUsd > 0 ? costUsd : undefined };
1844
+ return {
1845
+ input_tokens: input,
1846
+ output_tokens: output,
1847
+ ...(hasCost ? {
1848
+ cost_usd: costUsd,
1849
+ cost_source: 'provider_reported',
1850
+ cost_accuracy: 'exact',
1851
+ } : {}),
1852
+ };
1809
1853
  }
1810
1854
  return null;
1811
1855
  } catch (e) {
package/src/worker.js CHANGED
@@ -3890,7 +3890,10 @@ export class AgentForgeWorker extends EventEmitter {
3890
3890
  let uiRepairNudgeCount = 0;
3891
3891
  let taskResult;
3892
3892
  let iterationMessage = finalMessage;
3893
- let totalUsage = { input_tokens: 0, output_tokens: 0, cost_usd: 0 };
3893
+ let totalUsage = { input_tokens: 0, output_tokens: 0 };
3894
+ let totalUsageCostUsd = 0;
3895
+ let usageSegments = 0;
3896
+ let usageCostSegments = 0;
3894
3897
  // Fallback tracking: used at most once per task
3895
3898
  let usedFallback = false;
3896
3899
  let toolFailureRetryCount = 0;
@@ -4175,10 +4178,19 @@ export class AgentForgeWorker extends EventEmitter {
4175
4178
  break;
4176
4179
  }
4177
4180
  if (taskResult?.usage) {
4178
- totalUsage.input_tokens += taskResult.usage.input_tokens || 0;
4179
- totalUsage.output_tokens += taskResult.usage.output_tokens || 0;
4181
+ const inputTokens = Number(taskResult.usage.input_tokens || 0);
4182
+ const outputTokens = Number(taskResult.usage.output_tokens || 0);
4183
+ totalUsage.input_tokens += Number.isFinite(inputTokens) ? inputTokens : 0;
4184
+ totalUsage.output_tokens += Number.isFinite(outputTokens) ? outputTokens : 0;
4185
+ if ((inputTokens > 0 || outputTokens > 0) || taskResult.usage.cost_usd != null) {
4186
+ usageSegments += 1;
4187
+ }
4180
4188
  if (taskResult.usage.cost_usd != null) {
4181
- totalUsage.cost_usd += taskResult.usage.cost_usd;
4189
+ const costUsd = Number(taskResult.usage.cost_usd);
4190
+ if (Number.isFinite(costUsd) && costUsd >= 0) {
4191
+ totalUsageCostUsd += costUsd;
4192
+ usageCostSegments += 1;
4193
+ }
4182
4194
  }
4183
4195
  }
4184
4196
 
@@ -5065,7 +5077,12 @@ export class AgentForgeWorker extends EventEmitter {
5065
5077
  console.log(`[${taskId}] ℹ️ Task succeeded with no text output — using default completion message`);
5066
5078
  }
5067
5079
  console.log(`[${taskId}] 📤 finalOutput="${finalOutput.slice(0,100)}" response=${finalOutput ? `"${finalOutput.slice(0,80)}"` : 'undefined'}`);
5068
- const hasUsage = totalUsage.input_tokens > 0 || totalUsage.output_tokens > 0 || totalUsage.cost_usd > 0;
5080
+ if (usageSegments > 0 && usageCostSegments === usageSegments) {
5081
+ totalUsage.cost_usd = totalUsageCostUsd;
5082
+ totalUsage.cost_source = 'provider_reported';
5083
+ totalUsage.cost_accuracy = 'exact';
5084
+ }
5085
+ const hasUsage = totalUsage.input_tokens > 0 || totalUsage.output_tokens > 0 || totalUsage.cost_usd != null;
5069
5086
  const completionMessage = {
5070
5087
  type: 'task_complete',
5071
5088
  taskId,