dual-brain 4.2.0 → 4.5.0

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.
@@ -21,6 +21,7 @@ import { existsSync, readFileSync } from 'fs';
21
21
  import { dirname, join } from 'path';
22
22
  import { fileURLToPath } from 'url';
23
23
  import { atomicWriteJSON } from './atomic-write.mjs';
24
+ import { logHookError } from './error-channel.mjs';
24
25
 
25
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
27
 
@@ -43,7 +44,9 @@ function emptySummary() {
43
44
 
44
45
  totals: {
45
46
  calls: 0,
46
- cost_estimate: 0,
47
+ activity_score: 0, // token-weighted 0-100 scale, not dollars
48
+ activity_raw: 0, // raw weighted token count before normalization
49
+ activity_basis: 'none', // 'actual' | 'estimated' | 'mixed'
47
50
  by_tier: {},
48
51
  by_provider: {},
49
52
  by_model: {},
@@ -79,7 +82,14 @@ function emptySummary() {
79
82
  };
80
83
  }
81
84
 
82
- const COST_PER_CALL = { search: 0.003, execute: 0.012, think: 0.055 };
85
+ // Tier-based fallback weights when actual token counts are unavailable (legacy entries).
86
+ // These are unitless activity weights, NOT dollar costs.
87
+ const TIER_ACTIVITY_WEIGHTS = { search: 3, execute: 10, think: 25 };
88
+
89
+ // Activity formula: (input_tokens * 1) + (output_tokens * 3), normalized to 0-100 per session.
90
+ // SESSION_ACTIVITY_CEILING is the raw token-weighted value that maps to score 100.
91
+ // Calibrated to ~200 moderate tool calls in a session.
92
+ const SESSION_ACTIVITY_CEILING = 5_000_000;
83
93
 
84
94
  /** @deprecated Use atomicWriteJSON directly. Kept as re-export for backward compat. */
85
95
  function atomicWrite(path, data) {
@@ -127,10 +137,27 @@ function applyEntry(summary, entry) {
127
137
  const tier = entry.tier || 'execute';
128
138
  const provider = entry.provider || 'claude';
129
139
  const model = entry.model || 'unknown';
130
- const cost = COST_PER_CALL[tier] || COST_PER_CALL.execute;
140
+
141
+ // Compute activity from actual tokens when available, else use tier-based fallback
142
+ const hasTokens = entry.input_tokens != null && entry.output_tokens != null;
143
+ const rawActivity = hasTokens
144
+ ? (entry.input_tokens * 1) + (entry.output_tokens * 3)
145
+ : TIER_ACTIVITY_WEIGHTS[tier] || TIER_ACTIVITY_WEIGHTS.execute;
131
146
 
132
147
  summary.totals.calls++;
133
- summary.totals.cost_estimate += cost;
148
+ summary.totals.activity_raw = (summary.totals.activity_raw || 0) + rawActivity;
149
+ summary.totals.activity_score = Math.min(100,
150
+ Math.round((summary.totals.activity_raw / SESSION_ACTIVITY_CEILING) * 100));
151
+
152
+ // Track whether scores are based on actual tokens or estimates
153
+ const prevBasis = summary.totals.activity_basis || 'none';
154
+ if (prevBasis === 'none') {
155
+ summary.totals.activity_basis = hasTokens ? 'actual' : 'estimated';
156
+ } else if (prevBasis === 'actual' && !hasTokens) {
157
+ summary.totals.activity_basis = 'mixed';
158
+ } else if (prevBasis === 'estimated' && hasTokens) {
159
+ summary.totals.activity_basis = 'mixed';
160
+ }
134
161
 
135
162
  summary.totals.by_tier[tier] = (summary.totals.by_tier[tier] || 0) + 1;
136
163
  summary.totals.by_provider[provider] = (summary.totals.by_provider[provider] || 0) + 1;