@link-assistant/hive-mind 1.57.3 → 1.58.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.
- package/CHANGELOG.md +41 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +3 -1
- package/src/claude.lib.mjs +8 -3
- package/src/codex.lib.mjs +31 -0
- package/src/config.lib.mjs +34 -1
- package/src/github-cost-info.lib.mjs +4 -1
- package/src/solve.config.lib.mjs +10 -0
- package/src/sub-session-size.lib.mjs +239 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.58.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3616130: Add `--sub-session-size` and `--disable-1m-context` options for Claude and Codex (issue #1706)
|
|
8
|
+
|
|
9
|
+
`--sub-session-size` (default: `150k`) caps the size of each sub-session
|
|
10
|
+
between auto-compaction events. It accepts a token count (`150k`, `1m`,
|
|
11
|
+
`200000`), a percentage of the model context window (`50%`), or `default`
|
|
12
|
+
to keep the tool's built-in threshold.
|
|
13
|
+
|
|
14
|
+
`--disable-1m-context` (default: `true`) opts out of the 1M extended
|
|
15
|
+
context window so models stay on their standard 200K-400K window. This
|
|
16
|
+
preserves reasoning quality and avoids the long-context price tier.
|
|
17
|
+
Use `--no-disable-1m-context` to allow 1M.
|
|
18
|
+
|
|
19
|
+
Both options work for `--tool claude` and `--tool codex`. For Claude Code
|
|
20
|
+
the wrapper sets `CLAUDE_CODE_DISABLE_1M_CONTEXT`,
|
|
21
|
+
`CLAUDE_CODE_AUTO_COMPACT_WINDOW`, and `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`
|
|
22
|
+
env vars (clamped per upstream's "lower-only" semantics). For Codex the
|
|
23
|
+
wrapper appends `-c model_context_window=200000` and
|
|
24
|
+
`-c model_auto_compact_token_limit=<tokens>` overrides.
|
|
25
|
+
|
|
26
|
+
Verbose mode logs the applied env vars and `-c` overrides so operators
|
|
27
|
+
can confirm they reached the spawned tool process.
|
|
28
|
+
|
|
29
|
+
- b341775: Hide the cost-estimation breakdown when the public and Anthropic numbers agree to within display precision (issue #1703)
|
|
30
|
+
|
|
31
|
+
Both the live `displayCostComparison` console output and the
|
|
32
|
+
`buildCostInfoString` markdown rendered into PR/issue comments previously
|
|
33
|
+
collapsed to the short `💰 Cost: $X.XXXXXX` form only when the two values
|
|
34
|
+
matched **exactly** at six decimal places. Real-world calls regularly produce
|
|
35
|
+
underlying values that differ by ~`1e-7` and round to **adjacent** displays
|
|
36
|
+
(e.g. `$11.219694` vs `$11.219693`); the rendered difference (`$-0.000000
|
|
37
|
+
(-0.00%)`) was therefore noise yet still printed three full lines. The guard
|
|
38
|
+
now triggers whenever `|public − anthropic|.toFixed(6) === '0.000000'`, which
|
|
39
|
+
preserves the existing behaviour at every meaningful (≥ `$0.000001`) delta and
|
|
40
|
+
adds short-form output for the boundary case from issue #1703. Regression
|
|
41
|
+
tests live in `tests/test-build-cost-info-string.mjs` and
|
|
42
|
+
`tests/test-display-cost-comparison.mjs`.
|
|
43
|
+
|
|
3
44
|
## 1.57.3
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -129,7 +129,9 @@ export const displayCostComparison = async (publicCost, anthropicCost, log) => {
|
|
|
129
129
|
const hasAnthropic = anthropicCost !== null && anthropicCost !== undefined;
|
|
130
130
|
const publicDec = hasPublic ? new Decimal(publicCost) : null;
|
|
131
131
|
const anthropicDec = hasAnthropic ? new Decimal(anthropicCost) : null;
|
|
132
|
-
|
|
132
|
+
// Issue #1703: also collapse to the short form when the rounded difference is below display precision,
|
|
133
|
+
// so reports like "Difference: $-0.000000 (-0.00%)" no longer waste two extra lines.
|
|
134
|
+
if (publicDec && anthropicDec && anthropicDec.minus(publicDec).abs().toFixed(6) === '0.000000') {
|
|
133
135
|
await log(`\n 💰 Cost: $${anthropicDec.toFixed(6)}`);
|
|
134
136
|
return;
|
|
135
137
|
}
|
package/src/claude.lib.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
|
25
25
|
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
26
26
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
27
27
|
import { classifyRetryableError, maybeSwitchToFallbackModel } from './tool-retry.lib.mjs';
|
|
28
|
+
import { resolveSubSessionSize } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
28
29
|
export { availableModels }; // Re-export for backward compatibility
|
|
29
30
|
export { fetchModelInfo };
|
|
30
31
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
@@ -761,9 +762,10 @@ export const executeClaudeCommand = async params => {
|
|
|
761
762
|
}
|
|
762
763
|
try {
|
|
763
764
|
const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
|
|
764
|
-
// Issue #
|
|
765
|
-
|
|
766
|
-
|
|
765
|
+
// Issue #1706: --sub-session-size + --disable-1m-context. Resolve here, then pass into getClaudeEnv along with the rest.
|
|
766
|
+
const { parsed: parsedSubSessionSize, contextWindowTokens } = await resolveSubSessionSize({ rawValue: argv.subSessionSize, tool: 'claude', modelId: effectiveModel, fetchModelInfo, log });
|
|
767
|
+
// Issue #817: streaming mode sets exitAfterStopDelayMs=60000 so the headless Claude process stays alive between NDJSON turns.
|
|
768
|
+
const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel, showThinkingContent: argv.showThinkingContent, exitAfterStopDelayMs: streamingInput ? 60_000 : undefined, disable1mContext: !!argv.disable1mContext, subSessionSize: parsedSubSessionSize, contextWindowTokens });
|
|
767
769
|
if (argv.verbose) claudeEnv.ANTHROPIC_LOG = 'debug';
|
|
768
770
|
const modelMaxOutputTokens = getMaxOutputTokensForModel(effectiveModel);
|
|
769
771
|
if (argv.verbose) {
|
|
@@ -772,6 +774,9 @@ export const executeClaudeCommand = async params => {
|
|
|
772
774
|
if (resolvedThinkingBudget !== undefined) await log(`📊 MAX_THINKING_TOKENS: ${resolvedThinkingBudget}`, { verbose: true });
|
|
773
775
|
if (claudeEnv.CLAUDE_CODE_EFFORT_LEVEL) await log(`📊 CLAUDE_CODE_EFFORT_LEVEL: ${claudeEnv.CLAUDE_CODE_EFFORT_LEVEL}`, { verbose: true });
|
|
774
776
|
if (claudeEnv.CLAUDE_CODE_SHOW_THINKING) await log(`📊 CLAUDE_CODE_SHOW_THINKING: ${claudeEnv.CLAUDE_CODE_SHOW_THINKING}`, { verbose: true });
|
|
777
|
+
// Issue #1706: log applied env vars (--disable-1m-context, --sub-session-size).
|
|
778
|
+
const sub1706 = ['CLAUDE_CODE_DISABLE_1M_CONTEXT', 'CLAUDE_CODE_AUTO_COMPACT_WINDOW', 'CLAUDE_AUTOCOMPACT_PCT_OVERRIDE'].filter(k => claudeEnv[k]).map(k => `${k}=${claudeEnv[k]}`);
|
|
779
|
+
if (sub1706.length) await log(`📊 ${sub1706.join(', ')}`, { verbose: true });
|
|
775
780
|
if (!isNewVersion && thinkLevel) await log(`📊 Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
|
|
776
781
|
}
|
|
777
782
|
const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
|
package/src/codex.lib.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs
|
|
|
25
25
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
26
26
|
import { defaultModels } from './models/index.mjs';
|
|
27
27
|
import { classifyRetryableError, getRetryDelayMs, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
28
|
+
import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
28
29
|
import Decimal from 'decimal.js-light';
|
|
29
30
|
|
|
30
31
|
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'];
|
|
@@ -741,6 +742,36 @@ export const executeCodexCommand = async params => {
|
|
|
741
742
|
}
|
|
742
743
|
codexArgs += ` --json --skip-git-repo-check -o ${shellQuote(lastMessageFile)} -c ${shellQuote(`model_reasoning_effort=${reasoningEffort}`)} -c ${shellQuote('model_reasoning_summary=auto')} --dangerously-bypass-approvals-and-sandbox`;
|
|
743
744
|
|
|
745
|
+
// Issue #1706: Append --disable-1m-context and --sub-session-size as Codex -c overrides.
|
|
746
|
+
let parsedSubSessionSize;
|
|
747
|
+
try {
|
|
748
|
+
parsedSubSessionSize = parseSubSessionSize(argv.subSessionSize);
|
|
749
|
+
} catch (parseError) {
|
|
750
|
+
await log(`⚠️ ${parseError.message}`, { level: 'warn' });
|
|
751
|
+
parsedSubSessionSize = { kind: 'default', tokens: null, percent: null, raw: '' };
|
|
752
|
+
}
|
|
753
|
+
let codexContextWindowTokens = null;
|
|
754
|
+
if (parsedSubSessionSize.kind === 'percent') {
|
|
755
|
+
try {
|
|
756
|
+
const codexModelMeta = await fetchModelInfo(mappedModel, { preferredProviderIds: ['openai'] });
|
|
757
|
+
codexContextWindowTokens = codexModelMeta?.limit?.context || null;
|
|
758
|
+
} catch {
|
|
759
|
+
codexContextWindowTokens = null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const disable1mArgs = buildCodexDisable1mContextConfigArgs(!!argv.disable1mContext);
|
|
763
|
+
for (const arg of disable1mArgs) {
|
|
764
|
+
codexArgs += ` ${shellQuote(arg)}`;
|
|
765
|
+
}
|
|
766
|
+
const subSessionSizeArgs = buildCodexSubSessionSizeConfigArgs(parsedSubSessionSize, { contextWindow: codexContextWindowTokens });
|
|
767
|
+
for (const arg of subSessionSizeArgs) {
|
|
768
|
+
codexArgs += ` ${shellQuote(arg)}`;
|
|
769
|
+
}
|
|
770
|
+
if (argv.verbose) {
|
|
771
|
+
if (disable1mArgs.length) await log(`📊 Codex --disable-1m-context: ${disable1mArgs.join(' ')}`, { verbose: true });
|
|
772
|
+
if (subSessionSizeArgs.length) await log(`📊 Codex --sub-session-size: ${subSessionSizeArgs.join(' ')}`, { verbose: true });
|
|
773
|
+
}
|
|
774
|
+
|
|
744
775
|
const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${codexPath} ${codexArgs})`;
|
|
745
776
|
|
|
746
777
|
await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
|
package/src/config.lib.mjs
CHANGED
|
@@ -413,9 +413,12 @@ export const supportsThinkingBudget = (version, minVersion = '2.1.12') => {
|
|
|
413
413
|
// Supports model-specific max output tokens for Opus 4.6 (Issue #1221)
|
|
414
414
|
// Sets CLAUDE_CODE_EFFORT_LEVEL for Opus 4.6 models (Issue #1238)
|
|
415
415
|
// Supports planModel/executionModel for opusplan mode (Issue #1223)
|
|
416
|
-
//
|
|
416
|
+
// Issue #1706: supports subSessionSize (parsed) + disable1mContext to cap
|
|
417
|
+
// auto-compaction sub-session size and opt out of the 1M extended context.
|
|
418
|
+
// See: https://code.claude.com/docs/en/env-vars and https://code.claude.com/docs/en/model-config
|
|
417
419
|
// ANTHROPIC_DEFAULT_OPUS_MODEL → model used in plan mode (and for 'opus' alias)
|
|
418
420
|
// ANTHROPIC_DEFAULT_SONNET_MODEL → model used in execution mode (and for 'sonnet' alias)
|
|
421
|
+
// CLAUDE_CODE_DISABLE_1M_CONTEXT, CLAUDE_CODE_AUTO_COMPACT_WINDOW, CLAUDE_AUTOCOMPACT_PCT_OVERRIDE
|
|
419
422
|
export const getClaudeEnv = (options = {}) => {
|
|
420
423
|
// Get max output tokens based on model (Issue #1221)
|
|
421
424
|
const maxOutputTokens = options.model ? getMaxOutputTokensForModel(options.model) : claudeCode.maxOutputTokens;
|
|
@@ -483,6 +486,36 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
483
486
|
env.ANTHROPIC_DEFAULT_SONNET_MODEL = String(options.executionModel);
|
|
484
487
|
}
|
|
485
488
|
|
|
489
|
+
// Issue #1706: --disable-1m-context. Sets CLAUDE_CODE_DISABLE_1M_CONTEXT=1.
|
|
490
|
+
if (options.disable1mContext) {
|
|
491
|
+
env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Issue #1706: --sub-session-size. Caller passes a pre-parsed descriptor and the
|
|
495
|
+
// model context window so we can convert percentages to absolute tokens.
|
|
496
|
+
if (options.subSessionSize && options.subSessionSize.kind && options.subSessionSize.kind !== 'default') {
|
|
497
|
+
const window = Number.isFinite(options.contextWindowTokens) && options.contextWindowTokens > 0 ? options.contextWindowTokens : null;
|
|
498
|
+
if (options.subSessionSize.kind === 'tokens') {
|
|
499
|
+
const tokens = options.subSessionSize.tokens;
|
|
500
|
+
if (Number.isFinite(tokens) && tokens > 0) {
|
|
501
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(tokens);
|
|
502
|
+
// Compute percentage relative to the context window so the override stays
|
|
503
|
+
// within Claude Code's "lower-only" semantics. Default to 95 when unknown.
|
|
504
|
+
let pct = 95;
|
|
505
|
+
if (window) {
|
|
506
|
+
pct = Math.max(1, Math.min(95, Math.round((tokens / window) * 100)));
|
|
507
|
+
}
|
|
508
|
+
env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(pct);
|
|
509
|
+
}
|
|
510
|
+
} else if (options.subSessionSize.kind === 'percent') {
|
|
511
|
+
const pct = Math.max(1, Math.min(95, Math.round(options.subSessionSize.percent)));
|
|
512
|
+
env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(pct);
|
|
513
|
+
if (window) {
|
|
514
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(window);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
486
519
|
return env;
|
|
487
520
|
};
|
|
488
521
|
|
|
@@ -30,7 +30,10 @@ export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricing
|
|
|
30
30
|
if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
|
|
31
31
|
const publicDec = hasPublic ? new Decimal(totalCostUSD) : null;
|
|
32
32
|
const anthropicDec = hasAnthropic ? new Decimal(anthropicTotalCostUSD) : null;
|
|
33
|
-
|
|
33
|
+
// Issue #1703: collapse to short form when the rounded difference is below 6-decimal display precision.
|
|
34
|
+
// Without this, near-matching values like $11.219694 vs $11.219693 still printed the full breakdown
|
|
35
|
+
// even though "Difference: $-0.000000 (-0.00%)" carries no meaningful information.
|
|
36
|
+
if (publicDec && anthropicDec && anthropicDec.minus(publicDec).abs().toFixed(6) === '0.000000') return `\n\n### 💰 Cost: **$${anthropicDec.toFixed(6)}**`;
|
|
34
37
|
let costInfo = '\n\n### 💰 **Cost estimation:**';
|
|
35
38
|
if (pricingInfo?.modelName) {
|
|
36
39
|
costInfo += `\n- Model: ${pricingInfo.modelName}`;
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -260,6 +260,16 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
260
260
|
description: 'Maximum thinking budget for calculating --think level mappings (default: 31999 for Claude Code). Values: off=0, low=max/4, medium=max/2, high=max*3/4, max=max.',
|
|
261
261
|
default: 31999,
|
|
262
262
|
},
|
|
263
|
+
'sub-session-size': {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Cap on sub-session size between auto-compaction events. Accepts a token count (e.g. 150k, 1m, 200000), a percentage of the model context window (e.g. 50%), or "default" to keep the tool\'s built-in threshold. Default: 150k. For Claude this maps to CLAUDE_CODE_AUTO_COMPACT_WINDOW + CLAUDE_AUTOCOMPACT_PCT_OVERRIDE env vars. For Codex this maps to -c model_auto_compact_token_limit. (Issue #1706)',
|
|
266
|
+
default: '150k',
|
|
267
|
+
},
|
|
268
|
+
'disable-1m-context': {
|
|
269
|
+
type: 'boolean',
|
|
270
|
+
description: 'Disable 1M extended context window so the model uses its standard 200K-400K window. Helps preserve reasoning quality and reduces cost. Default: true. For Claude this sets CLAUDE_CODE_DISABLE_1M_CONTEXT=1 (also forbids the [1m] model-name suffix). For Codex this sets -c model_context_window=200000. Use --no-disable-1m-context to allow the 1M window. (Issue #1706)',
|
|
271
|
+
default: true,
|
|
272
|
+
},
|
|
263
273
|
'fallback-model': {
|
|
264
274
|
type: 'string',
|
|
265
275
|
description: 'Fallback model to switch to on model capacity/overload errors. When supported, retries resume the same session with this model. Defaults: claude opus/opus-4-7 -> opus-4-6; codex gpt-5.5 -> gpt-5.4; all others unset.',
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sub-session size and 1M-context controls.
|
|
5
|
+
*
|
|
6
|
+
* Implements --sub-session-size and --disable-1m-context (issue #1706).
|
|
7
|
+
*
|
|
8
|
+
* --sub-session-size accepts:
|
|
9
|
+
* - "default" / "auto" → keep tool's built-in compaction threshold (no override)
|
|
10
|
+
* - "off" / "0" → keep tool's default (alias for "default")
|
|
11
|
+
* - A token count → "150k", "150K", "150000", "1.5m", "1M"
|
|
12
|
+
* - A percentage → "50%", "75%" (relative to model context window)
|
|
13
|
+
*
|
|
14
|
+
* --disable-1m-context (boolean, default true) opts out of the 1M extended
|
|
15
|
+
* context window so models fall back to their standard 200K-400K window.
|
|
16
|
+
*
|
|
17
|
+
* Claude Code controls (env vars only — no CLI flags exist):
|
|
18
|
+
* - CLAUDE_CODE_DISABLE_1M_CONTEXT=1
|
|
19
|
+
* - CLAUDE_CODE_AUTO_COMPACT_WINDOW=<tokens> (basis for compaction math)
|
|
20
|
+
* - CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=<1..100> (only lowers; clamped to <= 95)
|
|
21
|
+
*
|
|
22
|
+
* Codex controls (via -c key=value, same mechanism as model_reasoning_effort):
|
|
23
|
+
* - -c model_context_window=<tokens> (forces 200K window)
|
|
24
|
+
* - -c model_auto_compact_token_limit=<tokens> (compaction threshold)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const PARSE_ERROR_PREFIX = '--sub-session-size';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TOKENS_VALUES = new Set(['default', 'auto', 'off', '0', 'none']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a token count expression: "150k", "150K", "150000", "1.5m", "1M".
|
|
33
|
+
* Returns null if the input doesn't match the token-count format.
|
|
34
|
+
*/
|
|
35
|
+
const parseTokenCount = value => {
|
|
36
|
+
const match = String(value)
|
|
37
|
+
.trim()
|
|
38
|
+
.match(/^(\d+(?:\.\d+)?)\s*([kmKM]?)$/);
|
|
39
|
+
if (!match) return null;
|
|
40
|
+
const number = parseFloat(match[1]);
|
|
41
|
+
if (!Number.isFinite(number) || number < 0) return null;
|
|
42
|
+
const suffix = match[2].toLowerCase();
|
|
43
|
+
const multiplier = suffix === 'k' ? 1_000 : suffix === 'm' ? 1_000_000 : 1;
|
|
44
|
+
return Math.round(number * multiplier);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a percentage expression: "50%", "75%".
|
|
49
|
+
* Returns null if the input doesn't match the percentage format.
|
|
50
|
+
*/
|
|
51
|
+
const parsePercent = value => {
|
|
52
|
+
const match = String(value)
|
|
53
|
+
.trim()
|
|
54
|
+
.match(/^(\d+(?:\.\d+)?)\s*%$/);
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
const percent = parseFloat(match[1]);
|
|
57
|
+
if (!Number.isFinite(percent) || percent <= 0 || percent > 100) return null;
|
|
58
|
+
return percent;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse the --sub-session-size option value into a normalized descriptor.
|
|
63
|
+
*
|
|
64
|
+
* @param {string|undefined|null} rawValue - The raw option value.
|
|
65
|
+
* @param {Object} [options]
|
|
66
|
+
* @param {number|null} [options.contextWindow] - Model context window in tokens (used for percentage values).
|
|
67
|
+
* @returns {{ kind: 'default' | 'tokens' | 'percent', tokens: number | null, percent: number | null, raw: string }}
|
|
68
|
+
* @throws {Error} If the value cannot be parsed.
|
|
69
|
+
*/
|
|
70
|
+
export const parseSubSessionSize = (rawValue, { contextWindow = null } = {}) => {
|
|
71
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
72
|
+
return { kind: 'default', tokens: null, percent: null, raw: '' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const trimmed = String(rawValue).trim();
|
|
76
|
+
const lower = trimmed.toLowerCase();
|
|
77
|
+
|
|
78
|
+
if (DEFAULT_TOKENS_VALUES.has(lower)) {
|
|
79
|
+
return { kind: 'default', tokens: null, percent: null, raw: trimmed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const percent = parsePercent(trimmed);
|
|
83
|
+
if (percent !== null) {
|
|
84
|
+
const tokens = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.round((contextWindow * percent) / 100) : null;
|
|
85
|
+
return { kind: 'percent', percent, tokens, raw: trimmed };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const tokens = parseTokenCount(trimmed);
|
|
89
|
+
if (tokens !== null) {
|
|
90
|
+
return { kind: 'tokens', tokens, percent: null, raw: trimmed };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(`${PARSE_ERROR_PREFIX}: invalid value "${rawValue}". Expected a token count (e.g. 150k, 1m), a percentage (e.g. 50%), or "default".`);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Apply --sub-session-size to a Claude Code env object.
|
|
98
|
+
*
|
|
99
|
+
* Claude Code uses CLAUDE_CODE_AUTO_COMPACT_WINDOW + CLAUDE_AUTOCOMPACT_PCT_OVERRIDE.
|
|
100
|
+
* The percentage override only *lowers* the default ~95% threshold (per upstream
|
|
101
|
+
* docs), so we clamp it at 95 to avoid silently being ignored.
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} env - Mutable env object to update.
|
|
104
|
+
* @param {Object} parsed - Result of parseSubSessionSize.
|
|
105
|
+
* @param {Object} [options]
|
|
106
|
+
* @param {number|null} [options.contextWindow] - Model context window in tokens.
|
|
107
|
+
* @returns {{ applied: boolean, summary: string|null }}
|
|
108
|
+
*/
|
|
109
|
+
export const applySubSessionSizeToClaudeEnv = (env, parsed, { contextWindow = null } = {}) => {
|
|
110
|
+
if (!parsed || parsed.kind === 'default') {
|
|
111
|
+
return { applied: false, summary: null };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const window = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : null;
|
|
115
|
+
|
|
116
|
+
if (parsed.kind === 'tokens') {
|
|
117
|
+
const tokens = parsed.tokens;
|
|
118
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return { applied: false, summary: null };
|
|
119
|
+
|
|
120
|
+
// Use the tokens value as the compaction window basis and apply 100%.
|
|
121
|
+
// Capped to the model's actual window by Claude Code itself.
|
|
122
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(tokens);
|
|
123
|
+
// Compute percentage relative to the model context window (if known) so the
|
|
124
|
+
// override stays within Claude Code's "lower-only" semantics. Default to 95.
|
|
125
|
+
let pct = 95;
|
|
126
|
+
if (window) {
|
|
127
|
+
pct = Math.max(1, Math.min(95, Math.round((tokens / window) * 100)));
|
|
128
|
+
}
|
|
129
|
+
env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(pct);
|
|
130
|
+
return {
|
|
131
|
+
applied: true,
|
|
132
|
+
summary: `CLAUDE_CODE_AUTO_COMPACT_WINDOW=${tokens}, CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=${pct}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (parsed.kind === 'percent') {
|
|
137
|
+
const pct = Math.max(1, Math.min(95, Math.round(parsed.percent)));
|
|
138
|
+
env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(pct);
|
|
139
|
+
if (window) {
|
|
140
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(window);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
applied: true,
|
|
144
|
+
summary: `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=${pct}${window ? `, CLAUDE_CODE_AUTO_COMPACT_WINDOW=${window}` : ''}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { applied: false, summary: null };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Apply --disable-1m-context to a Claude Code env object.
|
|
153
|
+
* Sets CLAUDE_CODE_DISABLE_1M_CONTEXT=1 when disabled is true.
|
|
154
|
+
*/
|
|
155
|
+
export const applyDisable1mContextToClaudeEnv = (env, disabled) => {
|
|
156
|
+
if (!disabled) return { applied: false };
|
|
157
|
+
env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
158
|
+
return { applied: true };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build Codex `-c` config args for --sub-session-size.
|
|
163
|
+
* Returns an array like ['-c', 'model_auto_compact_token_limit=150000'] or [].
|
|
164
|
+
*/
|
|
165
|
+
export const buildCodexSubSessionSizeConfigArgs = (parsed, { contextWindow = null } = {}) => {
|
|
166
|
+
if (!parsed || parsed.kind === 'default') return [];
|
|
167
|
+
|
|
168
|
+
let tokens = null;
|
|
169
|
+
if (parsed.kind === 'tokens') {
|
|
170
|
+
tokens = parsed.tokens;
|
|
171
|
+
} else if (parsed.kind === 'percent') {
|
|
172
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return [];
|
|
173
|
+
tokens = Math.round((contextWindow * parsed.percent) / 100);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return [];
|
|
177
|
+
return ['-c', `model_auto_compact_token_limit=${tokens}`];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build Codex `-c` config args for --disable-1m-context.
|
|
182
|
+
*
|
|
183
|
+
* Codex doesn't have a 1M-specific opt-out flag, but setting
|
|
184
|
+
* `model_context_window=200000` forces the standard window.
|
|
185
|
+
*
|
|
186
|
+
* @param {boolean} disabled - True when --disable-1m-context is in effect.
|
|
187
|
+
* @param {Object} [options]
|
|
188
|
+
* @param {number} [options.fallbackTokens] - Tokens to set when disabling (default: 200_000).
|
|
189
|
+
* @returns {string[]} Codex `-c` args, possibly empty.
|
|
190
|
+
*/
|
|
191
|
+
export const buildCodexDisable1mContextConfigArgs = (disabled, { fallbackTokens = 200_000 } = {}) => {
|
|
192
|
+
if (!disabled) return [];
|
|
193
|
+
return ['-c', `model_context_window=${fallbackTokens}`];
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve --sub-session-size for a given tool, including fetching the model
|
|
198
|
+
* context window when a percentage is provided. Tolerates fetch failures.
|
|
199
|
+
*
|
|
200
|
+
* @param {Object} params
|
|
201
|
+
* @param {string|undefined|null} params.rawValue - The argv.subSessionSize value.
|
|
202
|
+
* @param {string} params.tool - 'claude' or 'codex'.
|
|
203
|
+
* @param {string} params.modelId - Model id (used for models.dev lookup when percent).
|
|
204
|
+
* @param {Function} [params.fetchModelInfo] - models.dev fetcher (injected for testability).
|
|
205
|
+
* @param {Function} [params.log] - log function (used for parse warnings).
|
|
206
|
+
* @returns {Promise<{ parsed: Object, contextWindowTokens: number|null }>}
|
|
207
|
+
*/
|
|
208
|
+
export const resolveSubSessionSize = async ({ rawValue, tool, modelId, fetchModelInfo, log }) => {
|
|
209
|
+
let parsed;
|
|
210
|
+
try {
|
|
211
|
+
parsed = parseSubSessionSize(rawValue);
|
|
212
|
+
} catch (parseError) {
|
|
213
|
+
if (log) await log(`⚠️ ${parseError.message}`, { level: 'warn' });
|
|
214
|
+
parsed = { kind: 'default', tokens: null, percent: null, raw: '' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let contextWindowTokens = null;
|
|
218
|
+
if (parsed.kind === 'percent' && typeof fetchModelInfo === 'function') {
|
|
219
|
+
try {
|
|
220
|
+
const baseModelId = String(modelId || '').replace(/\[1m\]$/i, '');
|
|
221
|
+
const preferredProviderIds = tool === 'codex' ? ['openai'] : ['anthropic'];
|
|
222
|
+
const meta = await fetchModelInfo(baseModelId, { preferredProviderIds });
|
|
223
|
+
contextWindowTokens = meta?.limit?.context || null;
|
|
224
|
+
} catch {
|
|
225
|
+
contextWindowTokens = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { parsed, contextWindowTokens };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export default {
|
|
233
|
+
parseSubSessionSize,
|
|
234
|
+
applySubSessionSizeToClaudeEnv,
|
|
235
|
+
applyDisable1mContextToClaudeEnv,
|
|
236
|
+
buildCodexSubSessionSizeConfigArgs,
|
|
237
|
+
buildCodexDisable1mContextConfigArgs,
|
|
238
|
+
resolveSubSessionSize,
|
|
239
|
+
};
|