@link-assistant/hive-mind 2.0.29 → 2.1.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 +7 -0
- package/package.json +1 -1
- package/src/config.lib.mjs +26 -10
- package/src/models/index.mjs +10 -4
- package/src/session-monitor.lib.mjs +35 -21
- package/src/session-store.lib.mjs +1 -1
- package/src/solve.config.lib.mjs +1 -1
- package/src/solve.escalate.lib.mjs +2 -0
- package/src/solve.mjs +1 -1
- package/src/solve.resource-diagnostics.lib.mjs +114 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4e21d2a: Fix Docker task disk-usage reporting so Telegram completion messages use task container writable-layer samples instead of filesystem-capacity resource markers from the parent deployment.
|
|
8
|
+
- fdbf448: Add full support for Claude Sonnet 5 (`claude-sonnet-5`) and make it the default model for `--tool claude`. The bare `sonnet` alias now resolves to `claude-sonnet-5` (previously `claude-sonnet-4-6`). Sonnet 5 supports 1M context (`[1m]`), the full effort ladder including `xhigh` and `max`, 128K max output tokens, and adaptive-thinking-only environment handling. The `sonnet-4-6`/`claude-sonnet-4-6` aliases are retained for backward compatibility. (Issue #2003)
|
|
9
|
+
|
|
3
10
|
## 2.0.29
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/package.json
CHANGED
package/src/config.lib.mjs
CHANGED
|
@@ -266,6 +266,22 @@ const isSonnet46OrLater = model => {
|
|
|
266
266
|
return m === 'sonnet' || m === 'sonnet-4-6' || m.includes('sonnet-4-6') || m.includes('sonnet-5');
|
|
267
267
|
};
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Check if a model is Claude Sonnet 5 (Issue #2003)
|
|
271
|
+
* Sonnet 5 (`claude-sonnet-5`) is the current default for `--tool claude` (the bare
|
|
272
|
+
* `sonnet` alias now resolves to it). Unlike Sonnet 4.6 it supports the `xhigh` effort
|
|
273
|
+
* level, up to 128k output tokens, and uses adaptive thinking only (extended/manual
|
|
274
|
+
* thinking with an explicit budget is unavailable), matching the Fable/Mythos 5 API
|
|
275
|
+
* constraints. See: https://www.anthropic.com/news/claude-sonnet-5
|
|
276
|
+
* @param {string} model - The model name or ID
|
|
277
|
+
* @returns {boolean} True if the model is Claude Sonnet 5
|
|
278
|
+
*/
|
|
279
|
+
export const isSonnet5 = model => {
|
|
280
|
+
if (!model) return false;
|
|
281
|
+
const m = model.toLowerCase();
|
|
282
|
+
return m === 'sonnet' || m === 'sonnet-5' || m.includes('sonnet-5');
|
|
283
|
+
};
|
|
284
|
+
|
|
269
285
|
const isMythosPreview = model => {
|
|
270
286
|
if (!model) return false;
|
|
271
287
|
return model.toLowerCase().includes('mythos');
|
|
@@ -328,11 +344,11 @@ export const supportsEffortLevel = model => {
|
|
|
328
344
|
/**
|
|
329
345
|
* Check if a model supports the xhigh effort level.
|
|
330
346
|
* Official docs list xhigh for Claude Fable 5, Claude Mythos 5, Claude Opus 4.7,
|
|
331
|
-
*
|
|
347
|
+
* Opus 4.8, and Sonnet 5 (Issue #1832, Issue #1875, Issue #2003).
|
|
332
348
|
* @param {string} model - The model name or ID
|
|
333
349
|
* @returns {boolean} True if the model supports xhigh effort
|
|
334
350
|
*/
|
|
335
|
-
export const supportsXHighEffortLevel = model => isFable5OrMythos5(model) || isOpus47(model);
|
|
351
|
+
export const supportsXHighEffortLevel = model => isFable5OrMythos5(model) || isOpus47(model) || isSonnet5(model);
|
|
336
352
|
|
|
337
353
|
/**
|
|
338
354
|
* Check if a model supports the max effort level.
|
|
@@ -344,14 +360,14 @@ export const supportsXHighEffortLevel = model => isFable5OrMythos5(model) || isO
|
|
|
344
360
|
export const supportsMaxEffortLevel = model => isFable5OrMythos5(model) || isMythosPreview(model) || isOpus47OrLater(model) || isOpus46(model) || isSonnet46OrLater(model);
|
|
345
361
|
|
|
346
362
|
/**
|
|
347
|
-
* Get the max output tokens for a specific model (Issue #1221, Issue #1875)
|
|
348
|
-
* Claude Fable 5 and Claude
|
|
349
|
-
* the Opus 4.6+ ceiling.
|
|
363
|
+
* Get the max output tokens for a specific model (Issue #1221, Issue #1875, Issue #2003)
|
|
364
|
+
* Claude Fable 5, Claude Mythos 5, and Claude Sonnet 5 support up to 128k output tokens,
|
|
365
|
+
* matching the Opus 4.6+ ceiling.
|
|
350
366
|
* @param {string} model - The model name or ID
|
|
351
367
|
* @returns {number} The max output tokens for the model
|
|
352
368
|
*/
|
|
353
369
|
export const getMaxOutputTokensForModel = model => {
|
|
354
|
-
if (isOpus46OrLater(model) || isFable5OrMythos5(model)) {
|
|
370
|
+
if (isOpus46OrLater(model) || isFable5OrMythos5(model) || isSonnet5(model)) {
|
|
355
371
|
return claudeCode.maxOutputTokensOpus46;
|
|
356
372
|
}
|
|
357
373
|
return claudeCode.maxOutputTokens;
|
|
@@ -564,12 +580,12 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
564
580
|
|
|
565
581
|
// Opus 4.7+ always uses adaptive thinking — MAX_THINKING_TOKENS has no effect (Issue #1620, Issue #1832)
|
|
566
582
|
// Opus 4.8 inherits this constraint: adaptive thinking is the only thinking mode.
|
|
567
|
-
// Claude Fable 5 and Claude
|
|
568
|
-
// thinking is unavailable and `thinking: {type: "disabled"}` is rejected,
|
|
569
|
-
// MAX_THINKING_TOKENS=0 would be invalid for them (Issue #1875).
|
|
583
|
+
// Claude Fable 5, Claude Mythos 5, and Claude Sonnet 5 are adaptive-thinking-only too:
|
|
584
|
+
// extended/manual thinking is unavailable and `thinking: {type: "disabled"}` is rejected,
|
|
585
|
+
// so a MAX_THINKING_TOKENS=0 would be invalid for them (Issue #1875, Issue #2003).
|
|
570
586
|
// For Opus 4.6 and earlier, MAX_THINKING_TOKENS controls extended thinking (Claude Code >= 2.1.12)
|
|
571
587
|
// Default is 0 (thinking disabled) per Issue #1238.
|
|
572
|
-
const adaptiveThinkingOnly = options.model && (isOpus47OrLater(options.model) || isFable5OrMythos5(options.model));
|
|
588
|
+
const adaptiveThinkingOnly = options.model && (isOpus47OrLater(options.model) || isFable5OrMythos5(options.model) || isSonnet5(options.model));
|
|
573
589
|
if (adaptiveThinkingOnly) {
|
|
574
590
|
// Remove any inherited MAX_THINKING_TOKENS from process.env — these models ignore it
|
|
575
591
|
delete env.MAX_THINKING_TOKENS;
|
package/src/models/index.mjs
CHANGED
|
@@ -29,10 +29,10 @@ const execFileAsync = promisify(execFile);
|
|
|
29
29
|
// ─── MODEL DATA ──────────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
// Claude models (Anthropic API)
|
|
32
|
-
// Updated for Opus 4.5/4.6/4.7/4.8, Sonnet 4.6, and Fable 5 / Mythos 5 support
|
|
33
|
-
// (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620, Issue #1832, Issue #1875)
|
|
32
|
+
// Updated for Opus 4.5/4.6/4.7/4.8, Sonnet 4.6/5, and Fable 5 / Mythos 5 support
|
|
33
|
+
// (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620, Issue #1832, Issue #1875, Issue #2003)
|
|
34
34
|
export const claudeModels = {
|
|
35
|
-
sonnet: 'claude-sonnet-
|
|
35
|
+
sonnet: 'claude-sonnet-5', // Sonnet 5 (default, Issue #2003)
|
|
36
36
|
opus: 'claude-opus-4-8', // Opus 4.8 (Issue #1832)
|
|
37
37
|
haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
38
38
|
'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
|
|
@@ -46,6 +46,7 @@ export const claudeModels = {
|
|
|
46
46
|
'mythos-5': 'claude-mythos-5', // Mythos 5 short alias
|
|
47
47
|
'claude-mythos-5': 'claude-mythos-5', // Mythos 5 full ID
|
|
48
48
|
// Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
|
|
49
|
+
'sonnet-5': 'claude-sonnet-5', // Sonnet 5 short alias (Issue #2003)
|
|
49
50
|
'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
|
|
50
51
|
'opus-4-8': 'claude-opus-4-8', // Opus 4.8 short alias (Issue #1832)
|
|
51
52
|
'opus-4-7': 'claude-opus-4-7', // Opus 4.7 short alias (backward compatibility)
|
|
@@ -56,6 +57,7 @@ export const claudeModels = {
|
|
|
56
57
|
// Version aliases for backward compatibility (Issue #1221, Issue #1329, Issue #1620, Issue #1832)
|
|
57
58
|
'claude-opus-4-8': 'claude-opus-4-8', // Opus 4.8 (Issue #1832)
|
|
58
59
|
'claude-opus-4-7': 'claude-opus-4-7', // Opus 4.7 (backward compatibility)
|
|
60
|
+
'claude-sonnet-5': 'claude-sonnet-5', // Sonnet 5 (Issue #2003)
|
|
59
61
|
'claude-sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
|
|
60
62
|
'claude-opus-4-6': 'claude-opus-4-6', // Opus 4.6 (backward compatibility)
|
|
61
63
|
'claude-opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5
|
|
@@ -207,10 +209,12 @@ export const MODELS_SUPPORTING_1M_CONTEXT = [
|
|
|
207
209
|
'claude-opus-4-7', // Opus 4.7 (Issue #1620)
|
|
208
210
|
'claude-opus-4-6',
|
|
209
211
|
'claude-opus-4-5-20251101',
|
|
212
|
+
'claude-sonnet-5', // Sonnet 5 — 1M context (Issue #2003)
|
|
210
213
|
'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
|
|
211
214
|
'claude-sonnet-4-5-20250929',
|
|
212
215
|
'claude-sonnet-4-5',
|
|
213
|
-
'sonnet', // Now maps to Sonnet
|
|
216
|
+
'sonnet', // Now maps to Sonnet 5 (Issue #2003)
|
|
217
|
+
'sonnet-5', // Short alias (Issue #2003)
|
|
214
218
|
'sonnet-4-6', // Short alias (Issue #1329)
|
|
215
219
|
'opus', // Now maps to Opus 4.8 (Issue #1832)
|
|
216
220
|
'opus-4-8', // Short alias (Issue #1832)
|
|
@@ -1148,6 +1152,8 @@ export const defaultFallbackModels = {
|
|
|
1148
1152
|
'claude-mythos-5': 'fable',
|
|
1149
1153
|
'claude-opus-4-8': 'opus-4-7',
|
|
1150
1154
|
'claude-opus-4-7': 'opus-4-6',
|
|
1155
|
+
// Claude Sonnet 5 falls back to the prior Sonnet generation (Issue #2003).
|
|
1156
|
+
'claude-sonnet-5': 'sonnet-4-6',
|
|
1151
1157
|
},
|
|
1152
1158
|
codex: {
|
|
1153
1159
|
'gpt-5.6-sol': 'gpt-5.5',
|
|
@@ -193,6 +193,15 @@ function isPersistableSession(sessionInfo) {
|
|
|
193
193
|
return Boolean(sessionInfo?.isolationBackend && sessionInfo?.sessionId);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
function persistSessionSnapshot(sessionName, sessionInfo) {
|
|
197
|
+
if (!sessionStore || !isPersistableSession(sessionInfo)) return;
|
|
198
|
+
try {
|
|
199
|
+
sessionStore.persist(sessionName, sessionInfo);
|
|
200
|
+
} catch {
|
|
201
|
+
/* best effort — persistence must never break monitoring */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
/**
|
|
197
206
|
* Look up the in-memory record for a session id (UUID for isolation sessions
|
|
198
207
|
* or the screen session name for non-isolation sessions). Returns null when no
|
|
@@ -347,7 +356,6 @@ export async function buildDiskDiagnosticsExtraSection(logPath, { verbose = fals
|
|
|
347
356
|
if (!logPath && !Number.isFinite(containerFilesystemStartBytes) && !Number.isFinite(containerFilesystemAfterBytes)) return '';
|
|
348
357
|
try {
|
|
349
358
|
const diskLib = await import('./solve.disk-diagnostics.lib.mjs');
|
|
350
|
-
const resourceLib = await import('./solve.resource-diagnostics.lib.mjs');
|
|
351
359
|
let logText = '';
|
|
352
360
|
if (logPath) {
|
|
353
361
|
try {
|
|
@@ -359,17 +367,11 @@ export async function buildDiskDiagnosticsExtraSection(logPath, { verbose = fals
|
|
|
359
367
|
}
|
|
360
368
|
}
|
|
361
369
|
const parsed = diskLib.parseDiskMarkers(logText);
|
|
362
|
-
|
|
363
|
-
const bestResourceMarker = resourceLib.selectBestDiskResourceMarker(parsedResources);
|
|
364
|
-
const solveStartResourceMarker = parsedResources.byPhase?.[resourceLib.RESOURCE_PHASE_SOLVE_START] || null;
|
|
365
|
-
const useResourceFallback = String(isolationBackend || '').toLowerCase() === 'docker' && !Number.isFinite(containerFilesystemAfterBytes) && Number.isFinite(bestResourceMarker?.disk?.usedBytes);
|
|
366
|
-
const effectiveContainerFilesystemAfterBytes = useResourceFallback ? bestResourceMarker.disk.usedBytes : containerFilesystemAfterBytes;
|
|
367
|
-
const effectiveContainerFilesystemStartBytes = useResourceFallback && Number.isFinite(solveStartResourceMarker?.disk?.usedBytes) ? solveStartResourceMarker.disk.usedBytes : containerFilesystemStartBytes;
|
|
368
|
-
if (!parsed.afterClone && !parsed.afterAgent && !Number.isFinite(effectiveContainerFilesystemStartBytes) && !Number.isFinite(effectiveContainerFilesystemAfterBytes)) return '';
|
|
370
|
+
if (!parsed.afterClone && !parsed.afterAgent && !Number.isFinite(containerFilesystemStartBytes) && !Number.isFinite(containerFilesystemAfterBytes)) return '';
|
|
369
371
|
return diskLib.formatDiskDiagnosticsBlock(parsed, {
|
|
370
372
|
isolationBackend,
|
|
371
|
-
containerFilesystemStartBytes
|
|
372
|
-
containerFilesystemAfterBytes
|
|
373
|
+
containerFilesystemStartBytes,
|
|
374
|
+
containerFilesystemAfterBytes,
|
|
373
375
|
});
|
|
374
376
|
} catch (error) {
|
|
375
377
|
if (verbose) {
|
|
@@ -399,6 +401,19 @@ async function getDockerContainerFilesystemSizeForSession(sessionName, sessionIn
|
|
|
399
401
|
}
|
|
400
402
|
}
|
|
401
403
|
|
|
404
|
+
async function refreshDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, { verbose = false, sizeProvider = null } = {}) {
|
|
405
|
+
const bytes = await getDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, { verbose, sizeProvider });
|
|
406
|
+
if (!Number.isFinite(bytes)) return null;
|
|
407
|
+
sessionInfo.containerFilesystemLastBytes = bytes;
|
|
408
|
+
sessionInfo.containerFilesystemLastObservedAt = new Date().toISOString();
|
|
409
|
+
persistSessionSnapshot(sessionName, sessionInfo);
|
|
410
|
+
return bytes;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getLastKnownDockerContainerFilesystemSize(sessionInfo) {
|
|
414
|
+
return Number.isFinite(sessionInfo?.containerFilesystemLastBytes) ? sessionInfo.containerFilesystemLastBytes : null;
|
|
415
|
+
}
|
|
416
|
+
|
|
402
417
|
function isSuccessfulTaskCompletion({ exitCode = null, status = null } = {}) {
|
|
403
418
|
const outcome = classifySessionOutcome({ exitCode, status });
|
|
404
419
|
if (outcome.failed) return false;
|
|
@@ -664,6 +679,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
664
679
|
let exitCode = null;
|
|
665
680
|
let statusResult = null;
|
|
666
681
|
let resolvedStatus = null;
|
|
682
|
+
let observedContainerFilesystemBytes = null;
|
|
667
683
|
|
|
668
684
|
if (sessionInfo.isolationBackend && sessionInfo.sessionId) {
|
|
669
685
|
// Isolation mode: use $ --status, with screen -ls only as a fallback
|
|
@@ -689,13 +705,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
689
705
|
// the log footer to learn whether it was killed.
|
|
690
706
|
if (statusResult?.logPath && sessionInfo.logPath !== statusResult.logPath) {
|
|
691
707
|
sessionInfo.logPath = statusResult.logPath;
|
|
692
|
-
|
|
693
|
-
try {
|
|
694
|
-
sessionStore.persist(sessionName, sessionInfo);
|
|
695
|
-
} catch {
|
|
696
|
-
/* best effort — persistence must never break monitoring */
|
|
697
|
-
}
|
|
698
|
-
}
|
|
708
|
+
persistSessionSnapshot(sessionName, sessionInfo);
|
|
699
709
|
}
|
|
700
710
|
} else {
|
|
701
711
|
// Issue #1586: Non-isolation screen sessions cannot reliably detect
|
|
@@ -717,6 +727,13 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
717
727
|
}
|
|
718
728
|
}
|
|
719
729
|
|
|
730
|
+
if (sessionInfo?.isolationBackend === 'docker') {
|
|
731
|
+
observedContainerFilesystemBytes = await refreshDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, {
|
|
732
|
+
verbose,
|
|
733
|
+
sizeProvider: options.dockerContainerSizeProvider,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
720
737
|
if (!stillRunning) {
|
|
721
738
|
console.log(`Session ${sessionName} has finished. Sending notification to chat ${sessionInfo.chatId}`);
|
|
722
739
|
|
|
@@ -828,10 +845,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
828
845
|
const diskExtraSections = [];
|
|
829
846
|
try {
|
|
830
847
|
const diskLogPath = statusResult?.logPath || sessionInfo?.logPath || null;
|
|
831
|
-
const containerFilesystemAfterBytes =
|
|
832
|
-
verbose,
|
|
833
|
-
sizeProvider: options.dockerContainerSizeProvider,
|
|
834
|
-
});
|
|
848
|
+
const containerFilesystemAfterBytes = Number.isFinite(observedContainerFilesystemBytes) ? observedContainerFilesystemBytes : getLastKnownDockerContainerFilesystemSize(sessionInfo);
|
|
835
849
|
const diskBlock = await buildDiskDiagnosticsExtraSection(diskLogPath, {
|
|
836
850
|
verbose,
|
|
837
851
|
readFile: options.readFile,
|
|
@@ -33,7 +33,7 @@ import path from 'node:path';
|
|
|
33
33
|
// excluded so the snapshot stays small and safe to reload.
|
|
34
34
|
// `args` (#1927 review follow-up) is persisted so a killed /solve can be resumed
|
|
35
35
|
// with its exact original invocation plus `--resume <lastSessionId>`.
|
|
36
|
-
const PERSISTABLE_FIELDS = ['chatId', 'messageId', 'startTime', 'url', 'command', 'isolationBackend', 'sessionId', 'containerFilesystemStartBytes', 'tool', 'infoBlock', 'urlContext', 'requesterUserId', 'showLimits', 'locale', 'logPath', 'args'];
|
|
36
|
+
const PERSISTABLE_FIELDS = ['chatId', 'messageId', 'startTime', 'url', 'command', 'isolationBackend', 'sessionId', 'containerFilesystemStartBytes', 'containerFilesystemLastBytes', 'containerFilesystemLastObservedAt', 'tool', 'infoBlock', 'urlContext', 'requesterUserId', 'showLimits', 'locale', 'logPath', 'args'];
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Resolve the directory durable bot state is written to. Honors
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -299,7 +299,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
299
299
|
},
|
|
300
300
|
think: {
|
|
301
301
|
type: 'string',
|
|
302
|
-
description: 'Thinking level hint. For Claude, translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, xhigh/max=31999) and to CLAUDE_CODE_EFFORT_LEVEL when supported. Fable 5/Mythos 5/Opus 4.8/4.7 support xhigh and max; Opus 4.6/Sonnet 4.6/Mythos Preview support max; Opus 4.5 uses high for xhigh/max. For Codex, mapped to reasoning effort (off=none, low=low, medium=medium, high=high, xhigh/max=xhigh).',
|
|
302
|
+
description: 'Thinking level hint. For Claude, translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, xhigh/max=31999) and to CLAUDE_CODE_EFFORT_LEVEL when supported. Fable 5/Mythos 5/Sonnet 5/Opus 4.8/4.7 support xhigh and max; Opus 4.6/Sonnet 4.6/Mythos Preview support max; Opus 4.5 uses high for xhigh/max. For Codex, mapped to reasoning effort (off=none, low=low, medium=medium, high=high, xhigh/max=xhigh).',
|
|
303
303
|
choices: ['off', 'low', 'medium', 'high', 'xhigh', 'max'],
|
|
304
304
|
default: undefined,
|
|
305
305
|
},
|
|
@@ -72,8 +72,10 @@ const TIER_ALIASES = {
|
|
|
72
72
|
'claude-haiku-4-5': 'haiku',
|
|
73
73
|
'claude-haiku-4-5-20251001': 'haiku',
|
|
74
74
|
sonnet: 'sonnet',
|
|
75
|
+
'sonnet-5': 'sonnet',
|
|
75
76
|
'sonnet-4-6': 'sonnet',
|
|
76
77
|
'sonnet-4-5': 'sonnet',
|
|
78
|
+
'claude-sonnet-5': 'sonnet',
|
|
77
79
|
'claude-sonnet-4-6': 'sonnet',
|
|
78
80
|
'claude-sonnet-4-5': 'sonnet',
|
|
79
81
|
opus: 'opus',
|
package/src/solve.mjs
CHANGED
|
@@ -106,7 +106,7 @@ configureGitHubRateLimitLogging({
|
|
|
106
106
|
enabled: argv.githubRateLimitsLogging === true,
|
|
107
107
|
log,
|
|
108
108
|
});
|
|
109
|
-
await recordResourceSnapshot({ phase: RESOURCE_PHASE_SOLVE_START, log, diskPath: '/', label: 'solve start' });
|
|
109
|
+
await recordResourceSnapshot({ phase: RESOURCE_PHASE_SOLVE_START, log, diskPath: '/', label: 'solve start', logExecutionContext: true }); // #2001: detect+report container context
|
|
110
110
|
|
|
111
111
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
112
112
|
// Conditionally import tool-specific functions after argv is parsed
|
|
@@ -34,6 +34,110 @@ function readLinuxMemAvailableBytes(readFileSync = fs.readFileSync, platform = p
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Detect where the current process is running so the solve command can scope
|
|
39
|
+
* per-task disk usage to the correct context (issue #2001).
|
|
40
|
+
*
|
|
41
|
+
* When solve runs inside a Docker/container isolation backend, the per-task
|
|
42
|
+
* disk measurement must come from container-scoped signals (the cloned working
|
|
43
|
+
* tree measured with `du` inside the container, plus the host-side
|
|
44
|
+
* `docker inspect --size` writable-layer size) rather than the whole-VM
|
|
45
|
+
* filesystem reported by `statfs('/')`. This helper makes that detection
|
|
46
|
+
* explicit and observable in the solve log.
|
|
47
|
+
*
|
|
48
|
+
* It is intentionally defensive: any injected `existsSync`/`readFileSync`
|
|
49
|
+
* implementation that lacks a method or throws is treated as "signal absent"
|
|
50
|
+
* rather than propagating an error, so callers never crash on detection.
|
|
51
|
+
*
|
|
52
|
+
* @returns {{ inContainer: boolean, runtime: string|null, indicators: string[] }}
|
|
53
|
+
*/
|
|
54
|
+
export function detectExecutionContext(options = {}) {
|
|
55
|
+
const { existsSync = fs.existsSync, readFileSync = fs.readFileSync, env = process.env, platform = process.platform } = options;
|
|
56
|
+
|
|
57
|
+
const indicators = [];
|
|
58
|
+
let runtime = null;
|
|
59
|
+
|
|
60
|
+
const safeExists = target => {
|
|
61
|
+
try {
|
|
62
|
+
return typeof existsSync === 'function' ? existsSync(target) === true : false;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const safeRead = target => {
|
|
69
|
+
try {
|
|
70
|
+
return typeof readFileSync === 'function' ? String(readFileSync(target, 'utf8')) : '';
|
|
71
|
+
} catch {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (safeExists('/.dockerenv')) {
|
|
77
|
+
indicators.push('/.dockerenv');
|
|
78
|
+
runtime = runtime || 'docker';
|
|
79
|
+
}
|
|
80
|
+
if (safeExists('/run/.containerenv')) {
|
|
81
|
+
indicators.push('/run/.containerenv');
|
|
82
|
+
runtime = runtime || 'podman';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (platform === 'linux') {
|
|
86
|
+
const cgroup = safeRead('/proc/1/cgroup');
|
|
87
|
+
if (cgroup) {
|
|
88
|
+
if (/\bdocker\b/.test(cgroup)) {
|
|
89
|
+
indicators.push('cgroup:docker');
|
|
90
|
+
runtime = runtime || 'docker';
|
|
91
|
+
}
|
|
92
|
+
if (/kubepods/.test(cgroup)) {
|
|
93
|
+
indicators.push('cgroup:kubepods');
|
|
94
|
+
runtime = runtime || 'kubernetes';
|
|
95
|
+
}
|
|
96
|
+
if (/libpod|podman/.test(cgroup)) {
|
|
97
|
+
indicators.push('cgroup:podman');
|
|
98
|
+
runtime = runtime || 'podman';
|
|
99
|
+
}
|
|
100
|
+
if (/containerd/.test(cgroup)) {
|
|
101
|
+
indicators.push('cgroup:containerd');
|
|
102
|
+
runtime = runtime || 'containerd';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const envObj = env && typeof env === 'object' ? env : {};
|
|
108
|
+
if (envObj.HIVE_MIND_ISOLATION || envObj.HIVE_MIND_CONTAINER) {
|
|
109
|
+
indicators.push('env:hive-mind-isolation');
|
|
110
|
+
runtime = runtime || 'container';
|
|
111
|
+
}
|
|
112
|
+
if (envObj.KUBERNETES_SERVICE_HOST) {
|
|
113
|
+
indicators.push('env:kubernetes');
|
|
114
|
+
runtime = runtime || 'kubernetes';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
inContainer: indicators.length > 0,
|
|
119
|
+
runtime,
|
|
120
|
+
indicators,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Human-readable one-liner describing the detected execution context, used to
|
|
126
|
+
* make the disk-usage scope explicit in the solve log.
|
|
127
|
+
*
|
|
128
|
+
* @param {ReturnType<typeof detectExecutionContext>} context
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function formatExecutionContextForLog(context) {
|
|
132
|
+
const ctx = context || detectExecutionContext();
|
|
133
|
+
if (ctx.inContainer) {
|
|
134
|
+
const runtime = ctx.runtime || 'container';
|
|
135
|
+
const detail = ctx.indicators.length ? ` (indicators: ${ctx.indicators.join(', ')})` : '';
|
|
136
|
+
return `🧭 Execution context: ${runtime} container${detail} — per-task disk usage is scoped to this container.`;
|
|
137
|
+
}
|
|
138
|
+
return '🧭 Execution context: host (no container isolation detected) — per-task disk usage is scoped to the working tree.';
|
|
139
|
+
}
|
|
140
|
+
|
|
37
141
|
export function captureResourceSnapshot(options = {}) {
|
|
38
142
|
const { phase = 'snapshot', diskPath = '/', now = () => new Date(), osImpl = os, fsImpl = fs, processImpl = process } = options;
|
|
39
143
|
|
|
@@ -278,9 +382,18 @@ export function formatResourceSnapshotForLog(snapshot, label = null) {
|
|
|
278
382
|
return lines.join('\n');
|
|
279
383
|
}
|
|
280
384
|
|
|
281
|
-
export async function recordResourceSnapshot({ phase, log, diskPath = '/', label = null, capture = captureResourceSnapshot } = {}) {
|
|
385
|
+
export async function recordResourceSnapshot({ phase, log, diskPath = '/', label = null, capture = captureResourceSnapshot, logExecutionContext = false, detectContext = detectExecutionContext } = {}) {
|
|
282
386
|
if (typeof log !== 'function') return null;
|
|
283
387
|
try {
|
|
388
|
+
// Issue #2001: optionally report the execution context (host vs container)
|
|
389
|
+
// so it is explicit that per-task disk usage is scoped to the container.
|
|
390
|
+
if (logExecutionContext) {
|
|
391
|
+
try {
|
|
392
|
+
await log(formatExecutionContextForLog(detectContext()));
|
|
393
|
+
} catch {
|
|
394
|
+
/* context detection is best-effort and must never block the snapshot */
|
|
395
|
+
}
|
|
396
|
+
}
|
|
284
397
|
const snapshot = capture({ phase, diskPath });
|
|
285
398
|
await log(formatResourceSnapshotForLog(snapshot, label));
|
|
286
399
|
return snapshot;
|