@link-assistant/hive-mind 1.78.5 → 1.78.6

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
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.78.6
4
+
5
+ ### Patch Changes
6
+
7
+ - cf85feb: Fix Codex sub-session budget display by parsing compact diagnostics and preserving compact-derived sub-session rows.
8
+
3
9
  ## 1.78.5
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.78.5",
3
+ "version": "1.78.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -548,10 +548,16 @@ export const buildCumulativeInputPhrase = ({ input, cacheWrites, cacheReads, for
548
548
  */
549
549
  const formatSubSessionsList = (subSessions, contextLimit, outputLimit) => {
550
550
  let result = '';
551
+ let hasEstimatedRows = false;
551
552
  for (let i = 0; i < subSessions.length; i++) {
552
553
  const sub = subSessions[i];
554
+ if (sub.estimated) hasEstimatedRows = true;
553
555
  const subPeakContext = sub.peakContextUsage || 0;
554
- result += formatContextOutputLine(subPeakContext, contextLimit, sub.outputTokens, outputLimit, `${i + 1}. `);
556
+ const line = formatContextOutputLine(subPeakContext, contextLimit, sub.outputTokens, outputLimit, `${i + 1}. `);
557
+ result += sub.estimated ? line.replace(`\n${i + 1}. `, `\n${i + 1}. ~`) : line;
558
+ }
559
+ if (hasEstimatedRows) {
560
+ result += '\n\n_Sub-session values are estimates from observed compact events; the Total line remains exact._';
555
561
  }
556
562
  return result;
557
563
  };
@@ -847,6 +853,7 @@ export const buildAgentBudgetStats = (tokenUsage, pricingInfo) => {
847
853
  const contextLimit = tokenUsage.contextLimit || pricingInfo?.modelInfo?.limit?.context || null;
848
854
  const outputLimit = tokenUsage.outputLimit || pricingInfo?.modelInfo?.limit?.output || null;
849
855
  const contextFillInputTokens = getExplicitContextFillInputTokens(tokenUsage) ?? getCumulativeContextInputTokens({ inputTokens, cacheWriteTokens });
856
+ const subSessions = Array.isArray(tokenUsage.subSessions) ? tokenUsage.subSessions : [];
850
857
 
851
858
  const modelUsageEntry = {
852
859
  inputTokens,
@@ -862,7 +869,8 @@ export const buildAgentBudgetStats = (tokenUsage, pricingInfo) => {
862
869
 
863
870
  return {
864
871
  modelUsage: { [modelId]: modelUsageEntry },
865
- subSessions: [],
872
+ subSessions,
873
+ compactifications: Array.isArray(tokenUsage.compactifications) ? tokenUsage.compactifications : tokenUsage.compactifications || null,
866
874
  inputTokens,
867
875
  cacheCreationTokens: cacheWriteTokens,
868
876
  cacheReadTokens,
package/src/codex.lib.mjs CHANGED
@@ -33,8 +33,9 @@ import { getCumulativeContextInputTokens } from './context-fill.lib.mjs';
33
33
  import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
34
34
  import Decimal from 'decimal.js-light';
35
35
 
36
- const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens', 'cache_write_tokens', 'cache_creation_input_tokens', 'reasoning_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens', 'output_tokens_details.reasoning_tokens'];
36
+ const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens', 'cache_write_tokens', 'cache_creation_input_tokens', 'reasoning_tokens', 'reasoning_output_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens', 'output_tokens_details.reasoning_tokens'];
37
37
  const CODEX_LONG_CONTEXT_PRICE_THRESHOLD = 272000;
38
+ const CODEX_COMPACT_API_ENDPOINT = '/responses/compact';
38
39
  const getCodexExecEnv = (verbose = false) => (verbose ? { ...process.env, RUST_LOG: 'debug' } : { ...process.env });
39
40
  const CODEX_MODEL_DIAGNOSTIC_PATHS = [
40
41
  ['model', data => data?.model],
@@ -76,7 +77,139 @@ const hasAnyObservedPath = (object, pathNames) => pathNames.some(pathName => has
76
77
 
77
78
  const CODEX_CACHE_READ_USAGE_PATHS = ['cached_input_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens'];
78
79
  const CODEX_CACHE_WRITE_USAGE_PATHS = ['cache_write_tokens', 'cache_creation_input_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens'];
79
- const CODEX_REASONING_USAGE_PATHS = ['reasoning_tokens', 'output_tokens_details.reasoning_tokens'];
80
+ const CODEX_REASONING_USAGE_PATHS = ['reasoning_tokens', 'reasoning_output_tokens', 'output_tokens_details.reasoning_tokens'];
81
+
82
+ const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
83
+
84
+ const getCodexDiagnosticValue = (line, key) => {
85
+ const match = line.match(new RegExp(`${escapeRegExp(key)}=(?:"([^"]*)"|([^\\s")]+))`));
86
+ return match?.[1] ?? match?.[2] ?? null;
87
+ };
88
+
89
+ const getCodexDiagnosticInteger = (line, key) => {
90
+ const value = getCodexDiagnosticValue(line, key);
91
+ if (value === null) return null;
92
+ const parsed = Number.parseInt(value, 10);
93
+ return Number.isFinite(parsed) ? parsed : null;
94
+ };
95
+
96
+ const getCodexDiagnosticTimestamp = line => {
97
+ const eventTimestamp = getCodexDiagnosticValue(line, 'event.timestamp');
98
+ if (eventTimestamp) return eventTimestamp;
99
+ const logPrefixMatch = line.match(/^\[(\d{4}-\d{2}-\d{2}T[^\]]+Z)\]/u);
100
+ return logPrefixMatch?.[1] ?? null;
101
+ };
102
+
103
+ const isSuccessfulCodexCompactRequestLine = line => {
104
+ if (!line.includes('codex_otel.log_only:')) return false;
105
+ if (!line.includes('event.name="codex.api_request"')) return false;
106
+ if (!line.includes(`endpoint="${CODEX_COMPACT_API_ENDPOINT}"`)) return false;
107
+ const statusCode = getCodexDiagnosticInteger(line, 'http.response.status_code');
108
+ return statusCode === null || (statusCode >= 200 && statusCode < 300);
109
+ };
110
+
111
+ const splitTokenCountEvenly = (total, partCount) => {
112
+ const safeTotal = Math.max(0, Math.round(total || 0));
113
+ const safePartCount = Math.max(1, Math.round(partCount || 1));
114
+ const base = Math.floor(safeTotal / safePartCount);
115
+ let remainder = safeTotal % safePartCount;
116
+ return Array.from({ length: safePartCount }, () => {
117
+ const value = base + (remainder > 0 ? 1 : 0);
118
+ if (remainder > 0) remainder--;
119
+ return value;
120
+ });
121
+ };
122
+
123
+ const splitCodexSubSessionInputTokens = (total, partCount, autoCompactTokenLimit = null) => {
124
+ const safeTotal = Math.max(0, Math.round(total || 0));
125
+ const safePartCount = Math.max(1, Math.round(partCount || 1));
126
+ const safeLimit = Number.isFinite(autoCompactTokenLimit) && autoCompactTokenLimit > 0 ? Math.round(autoCompactTokenLimit) : null;
127
+ if (safePartCount <= 1) return [safeTotal];
128
+ if (safeLimit && safeTotal > safeLimit * (safePartCount - 1)) {
129
+ const chunks = [];
130
+ let remaining = safeTotal;
131
+ for (let i = 0; i < safePartCount - 1; i++) {
132
+ const chunk = Math.min(safeLimit, remaining);
133
+ chunks.push(chunk);
134
+ remaining -= chunk;
135
+ }
136
+ chunks.push(Math.max(0, remaining));
137
+ return chunks;
138
+ }
139
+ return splitTokenCountEvenly(safeTotal, safePartCount);
140
+ };
141
+
142
+ const splitTokenCountByWeights = (total, weights) => {
143
+ const safeTotal = Math.max(0, Math.round(total || 0));
144
+ const safeWeights = Array.isArray(weights) && weights.length > 0 ? weights.map(weight => Math.max(0, weight || 0)) : [1];
145
+ const weightTotal = safeWeights.reduce((sum, weight) => sum + weight, 0);
146
+ if (weightTotal <= 0) return splitTokenCountEvenly(safeTotal, safeWeights.length);
147
+
148
+ let allocated = 0;
149
+ return safeWeights.map((weight, index) => {
150
+ if (index === safeWeights.length - 1) return Math.max(0, safeTotal - allocated);
151
+ const value = Math.floor((safeTotal * weight) / weightTotal);
152
+ allocated += value;
153
+ return value;
154
+ });
155
+ };
156
+
157
+ const rebuildCodexSubSessionsFromCompactifications = tokenUsage => {
158
+ const compactifications = Array.isArray(tokenUsage.compactifications) ? tokenUsage.compactifications : [];
159
+ if (compactifications.length === 0 || (tokenUsage.stepCount || 0) === 0) {
160
+ tokenUsage.subSessions = Array.isArray(tokenUsage.subSessions) ? tokenUsage.subSessions : [];
161
+ return;
162
+ }
163
+
164
+ const subSessionCount = compactifications.length + 1;
165
+ const inputChunks = splitCodexSubSessionInputTokens(tokenUsage.inputTokens || 0, subSessionCount, tokenUsage.autoCompactTokenLimit);
166
+ const cacheWriteChunks = splitTokenCountByWeights(tokenUsage.cacheWriteTokens || 0, inputChunks);
167
+ const cacheReadChunks = splitTokenCountByWeights(tokenUsage.cacheReadTokens || 0, inputChunks);
168
+ const outputChunks = splitTokenCountByWeights(tokenUsage.outputTokens || 0, inputChunks);
169
+
170
+ tokenUsage.subSessions = inputChunks.map((inputTokens, index) => {
171
+ const cacheCreationTokens = cacheWriteChunks[index] || 0;
172
+ const outputTokens = outputChunks[index] || 0;
173
+ return {
174
+ inputTokens,
175
+ cacheCreationTokens,
176
+ cacheReadTokens: cacheReadChunks[index] || 0,
177
+ outputTokens,
178
+ messageCount: null,
179
+ peakContextUsage: getCumulativeContextInputTokens({ inputTokens, cacheCreationTokens }),
180
+ peakOutputUsage: outputTokens,
181
+ estimated: true,
182
+ source: 'codex.compact-diagnostics',
183
+ compactBoundaryBefore: index === 0 ? null : compactifications[index - 1] || null,
184
+ };
185
+ });
186
+ };
187
+
188
+ const recordCodexCompactification = (line, tokenUsage) => {
189
+ if (!isSuccessfulCodexCompactRequestLine(line)) return;
190
+ const timestamp = getCodexDiagnosticTimestamp(line);
191
+ const conversationId = getCodexDiagnosticValue(line, 'conversation.id');
192
+ const existing = tokenUsage.compactifications.find(compact => compact.timestamp === timestamp && compact.conversationId === conversationId);
193
+ if (existing) return;
194
+
195
+ tokenUsage.compactifications.push({
196
+ timestamp,
197
+ preTokens: null,
198
+ trigger: 'auto',
199
+ source: 'codex.responses.compact',
200
+ conversationId: conversationId || null,
201
+ });
202
+ };
203
+
204
+ const parseCodexDiagnosticLine = (line, tokenUsage) => {
205
+ const contextLimit = getCodexDiagnosticInteger(line, 'context_window') ?? getCodexDiagnosticInteger(line, 'model_context_window');
206
+ if (contextLimit !== null) tokenUsage.contextLimit = contextLimit;
207
+
208
+ const autoCompactTokenLimit = getCodexDiagnosticInteger(line, 'auto_compact_token_limit') ?? getCodexDiagnosticInteger(line, 'model_auto_compact_token_limit');
209
+ if (autoCompactTokenLimit !== null) tokenUsage.autoCompactTokenLimit = autoCompactTokenLimit;
210
+
211
+ recordCodexCompactification(line, tokenUsage);
212
+ };
80
213
 
81
214
  export const createCodexTokenUsage = requestedModelId => ({
82
215
  inputTokens: 0,
@@ -90,8 +223,11 @@ export const createCodexTokenUsage = requestedModelId => ({
90
223
  respondedModelId: requestedModelId || null,
91
224
  contextLimit: null,
92
225
  outputLimit: null,
226
+ autoCompactTokenLimit: null,
93
227
  contextFillInputTokens: 0,
94
228
  peakContextUsage: 0,
229
+ subSessions: [],
230
+ compactifications: [],
95
231
  tokenFieldAvailability: createCodexTokenFieldAvailability(),
96
232
  });
97
233
 
@@ -285,12 +421,17 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
285
421
  };
286
422
 
287
423
  nextState.tokenUsage.tokenFieldAvailability ||= createCodexTokenFieldAvailability();
424
+ if (!Array.isArray(nextState.tokenUsage.subSessions)) nextState.tokenUsage.subSessions = [];
425
+ if (!Array.isArray(nextState.tokenUsage.compactifications)) nextState.tokenUsage.compactifications = [];
426
+ nextState.tokenUsage.autoCompactTokenLimit ??= null;
288
427
  const observedModelPaths = new Set(nextState.observedModelDiagnosticPaths);
289
428
 
290
429
  for (const rawLine of output.split('\n')) {
291
430
  const line = rawLine.trim();
292
431
  if (!line) continue;
293
432
 
433
+ parseCodexDiagnosticLine(line, nextState.tokenUsage);
434
+
294
435
  let data;
295
436
  try {
296
437
  data = sanitizeObjectStrings(JSON.parse(line));
@@ -405,6 +546,7 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
405
546
  }
406
547
  }
407
548
 
549
+ rebuildCodexSubSessionsFromCompactifications(nextState.tokenUsage);
408
550
  nextState.observedModelDiagnosticPaths = [...observedModelPaths];
409
551
  return nextState;
410
552
  };
@@ -901,6 +1043,7 @@ export const executeCodexCommand = async params => {
901
1043
  if (errorOutput && argv.verbose) {
902
1044
  await log(errorOutput, { stream: 'stderr' });
903
1045
  }
1046
+ codexJsonState = parseCodexExecJsonOutput(errorOutput, codexJsonState, mappedModel);
904
1047
  } else if (chunk.type === 'exit') {
905
1048
  exitCode = chunk.code;
906
1049
  }