@link-assistant/hive-mind 2.7.1 → 2.7.3
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 +12 -0
- package/package.json +1 -1
- package/src/anthropic-cost-accumulator.lib.mjs +38 -15
- package/src/auto-resume-uncommitted.lib.mjs +148 -0
- package/src/claude.lib.mjs +5 -3
- package/src/option-suggestions.lib.mjs +2 -0
- package/src/solve.auto-continue.lib.mjs +5 -1
- package/src/solve.config.lib.mjs +10 -0
- package/src/solve.mjs +2 -2
- package/src/solve.watch.lib.mjs +51 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.7.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e6c67c0: Reset Anthropic cost accounting between fresh auto-restart working sessions while preserving cumulative totals for true session resumes.
|
|
8
|
+
|
|
9
|
+
## 2.7.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c6add61: Add experimental `--auto-resume-on-uncommitted-changes` flag (#1056) that complements the existing `--auto-restart-on-uncommitted-changes` by reusing the previous Claude Code session via `--resume <sessionId>` when uncommitted changes are detected, preserving the agent's accumulated context instead of starting a fresh session. The flag is disabled by default. A companion knob, `--auto-resume-on-uncommitted-changes-maximum-context-window-usage` (default 50%), bounds the worst-case peak usage of the usable pre-compaction context (respecting `--sub-session-size`); sessions at or above the threshold, or sessions whose usage cannot be verified, fall back to a fresh run.
|
|
14
|
+
|
|
3
15
|
## 2.7.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -29,12 +29,11 @@
|
|
|
29
29
|
* shown next to the full-session public estimate covers the same scope. This
|
|
30
30
|
* module is the single source of truth for that running total:
|
|
31
31
|
*
|
|
32
|
-
* - Each `
|
|
33
|
-
* `--previous-anthropic-cost`
|
|
34
|
-
*
|
|
35
|
-
* - Every finished Claude process adds its own
|
|
36
|
-
* `
|
|
37
|
-
* (each iteration is a separate Claude process in the same node process).
|
|
32
|
+
* - Each `executeClaude` call starts a scope. Fresh sessions reset it; true
|
|
33
|
+
* resumes retain it or seed it from `--previous-anthropic-cost` when the
|
|
34
|
+
* resume crosses a process boundary.
|
|
35
|
+
* - Every finished Claude process in that logical session adds its own
|
|
36
|
+
* `total_cost_usd` via `addAnthropicRunCost`.
|
|
38
37
|
* - The display and the cross-process spawn both read the cumulative total,
|
|
39
38
|
* so "Calculated by Anthropic" tracks the full session.
|
|
40
39
|
*
|
|
@@ -43,12 +42,12 @@
|
|
|
43
42
|
* future model. See docs/case-studies/issue-1886/ for the full analysis.
|
|
44
43
|
*/
|
|
45
44
|
|
|
46
|
-
// Module-level singleton: the cumulative Anthropic cost for the
|
|
47
|
-
// session (
|
|
45
|
+
// Module-level singleton: the cumulative Anthropic cost for the active logical
|
|
46
|
+
// session (including anything seeded by a true resume from a prior process).
|
|
48
47
|
let cumulativeAnthropicCostUSD = 0;
|
|
49
|
-
// Seeding must happen exactly once per
|
|
50
|
-
//
|
|
51
|
-
//
|
|
48
|
+
// Seeding must happen exactly once per active scope. Resume retries may call the
|
|
49
|
+
// seed helper repeatedly; re-seeding from the same CLI flag would wipe out the
|
|
50
|
+
// costs already accumulated in this process.
|
|
52
51
|
let seeded = false;
|
|
53
52
|
|
|
54
53
|
/**
|
|
@@ -61,10 +60,34 @@ const toCostAmount = value => {
|
|
|
61
60
|
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
62
61
|
};
|
|
63
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Start accounting for one logical Claude session.
|
|
65
|
+
*
|
|
66
|
+
* A fresh execution owns a new transcript and therefore a new cost scope. A
|
|
67
|
+
* true `--resume` execution continues the active scope (or restores it from
|
|
68
|
+
* `--previous-anthropic-cost` when the resume happens in a child process).
|
|
69
|
+
* Calling this at the public `executeClaude` boundary keeps retries inside one
|
|
70
|
+
* scope while preventing watch-mode and mergeability auto-restarts from
|
|
71
|
+
* inheriting the preceding working session's cost.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} [options]
|
|
74
|
+
* @param {boolean|string} [options.resume=false] - whether this execution resumes an existing Claude session
|
|
75
|
+
* @param {number|string|null} [options.previousAnthropicCost=0] - cumulative cost restored by a resumed child process
|
|
76
|
+
* @returns {number} the cumulative cost at the start of this execution
|
|
77
|
+
*/
|
|
78
|
+
export const beginAnthropicCostScope = ({ resume = false, previousAnthropicCost = 0 } = {}) => {
|
|
79
|
+
if (!resume) {
|
|
80
|
+
cumulativeAnthropicCostUSD = 0;
|
|
81
|
+
seeded = true;
|
|
82
|
+
return cumulativeAnthropicCostUSD;
|
|
83
|
+
}
|
|
84
|
+
return seedCumulativeAnthropicCost(previousAnthropicCost);
|
|
85
|
+
};
|
|
86
|
+
|
|
64
87
|
/**
|
|
65
88
|
* Seed the accumulator from the carried-forward previous-run cost, exactly once
|
|
66
|
-
* per
|
|
67
|
-
*
|
|
89
|
+
* per active scope. Subsequent calls are no-ops so resume retries do not reset
|
|
90
|
+
* the running total.
|
|
68
91
|
* @param {number|string|null|undefined} previousAnthropicCostUSD
|
|
69
92
|
* @returns {number} the cumulative total after seeding
|
|
70
93
|
*/
|
|
@@ -98,8 +121,8 @@ export const getCumulativeAnthropicCost = () => cumulativeAnthropicCostUSD;
|
|
|
98
121
|
export const hasCumulativeAnthropicCost = () => cumulativeAnthropicCostUSD > 0;
|
|
99
122
|
|
|
100
123
|
/**
|
|
101
|
-
* Reset the accumulator. Intended for tests
|
|
102
|
-
*
|
|
124
|
+
* Reset the accumulator. Intended for tests; production code starts scopes via
|
|
125
|
+
* `beginAnthropicCostScope`.
|
|
103
126
|
*/
|
|
104
127
|
export const resetCumulativeAnthropicCost = () => {
|
|
105
128
|
cumulativeAnthropicCostUSD = 0;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auto-resume on uncommitted changes — decision helpers.
|
|
5
|
+
*
|
|
6
|
+
* Issue #1056: when uncommitted changes are detected and the user has
|
|
7
|
+
* enabled `--auto-resume-on-uncommitted-changes`, we want to call the
|
|
8
|
+
* agent again with `--resume <sessionId>` (preserving context) instead
|
|
9
|
+
* of starting a fresh session — but only when the previous session has
|
|
10
|
+
* not already filled most of its usable pre-compaction context. The threshold
|
|
11
|
+
* defaults to 50% of that usable limit and is configurable via
|
|
12
|
+
* `--auto-resume-on-uncommitted-changes-maximum-context-window-usage`.
|
|
13
|
+
*
|
|
14
|
+
* This module is intentionally tool-agnostic. It does not perform the
|
|
15
|
+
* resume itself — it just decides whether resuming is viable and
|
|
16
|
+
* computes the percentage that the caller can log.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { parseSubSessionSize } from './sub-session-size.lib.mjs';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_MAX_CONTEXT_USAGE_PERCENT = 50;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the configured max-context-usage threshold (in percent) from argv.
|
|
25
|
+
*
|
|
26
|
+
* Accepts both the camelCase form populated by yargs
|
|
27
|
+
* (`autoResumeOnUncommittedChangesMaximumContextWindowUsage`) and the
|
|
28
|
+
* dash-cased flag itself, so that programmatic callers and CLI users
|
|
29
|
+
* see the same default.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} argv - parsed CLI arguments
|
|
32
|
+
* @returns {number} threshold in [0, 100]
|
|
33
|
+
*/
|
|
34
|
+
export const getAutoResumeMaxContextUsage = (argv = {}) => {
|
|
35
|
+
const candidates = [argv.autoResumeOnUncommittedChangesMaximumContextWindowUsage, argv['auto-resume-on-uncommitted-changes-maximum-context-window-usage']];
|
|
36
|
+
for (const value of candidates) {
|
|
37
|
+
if (value === undefined || value === null || value === '') continue;
|
|
38
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
39
|
+
if (Number.isFinite(parsed)) return Math.max(0, Math.min(100, parsed));
|
|
40
|
+
}
|
|
41
|
+
return DEFAULT_MAX_CONTEXT_USAGE_PERCENT;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether `--auto-resume-on-uncommitted-changes` is enabled.
|
|
46
|
+
* @param {Object} argv
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
export const isAutoResumeOnUncommittedChangesEnabled = (argv = {}) => {
|
|
50
|
+
return argv.autoResumeOnUncommittedChanges === true || argv['auto-resume-on-uncommitted-changes'] === true;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pick the largest peak-context-input across all models in a token-usage map
|
|
55
|
+
* that has a known model context limit, and return both the peak and the
|
|
56
|
+
* matching limit. We use the worst (highest-utilisation) model so that
|
|
57
|
+
* resuming with multi-model sessions does not silently exceed the threshold
|
|
58
|
+
* for the model that has the smallest remaining headroom.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object|null} tokenUsage - shape returned by calculateSessionTokens
|
|
61
|
+
* @returns {{peak: number, limit: number, contextLimit: number, ratio: number}|null} null when no model with verified usage and a known limit was found
|
|
62
|
+
*/
|
|
63
|
+
export const pickWorstContextUtilisation = (tokenUsage, argv = {}) => {
|
|
64
|
+
if (!tokenUsage || !tokenUsage.modelUsage) return null;
|
|
65
|
+
let worst = null;
|
|
66
|
+
for (const usage of Object.values(tokenUsage.modelUsage)) {
|
|
67
|
+
const contextLimit = usage?.modelInfo?.limit?.context;
|
|
68
|
+
if (!contextLimit || contextLimit <= 0) continue;
|
|
69
|
+
let limit = contextLimit;
|
|
70
|
+
try {
|
|
71
|
+
const configured = argv.subSessionSize ?? argv['sub-session-size'];
|
|
72
|
+
const subSession = parseSubSessionSize(configured, { contextWindow: contextLimit });
|
|
73
|
+
if (subSession.kind === 'tokens' && subSession.tokens > 0) {
|
|
74
|
+
limit = Math.min(contextLimit, subSession.tokens);
|
|
75
|
+
} else if (subSession.kind === 'percent' && subSession.tokens > 0) {
|
|
76
|
+
limit = Math.min(contextLimit, subSession.tokens);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Invalid values are reported by normal CLI validation. Programmatic
|
|
80
|
+
// callers still get a conservative decision against the model limit.
|
|
81
|
+
}
|
|
82
|
+
const peak = usage.peakContextUsage;
|
|
83
|
+
if (!Number.isFinite(peak) || peak <= 0) continue;
|
|
84
|
+
const ratio = peak / limit;
|
|
85
|
+
if (!worst || ratio > worst.ratio) worst = { peak, limit, contextLimit, ratio };
|
|
86
|
+
}
|
|
87
|
+
return worst;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Decide whether resuming is viable given a session ID and the previous
|
|
92
|
+
* session's token-usage data. Returns a structured result that the caller
|
|
93
|
+
* can log and act on.
|
|
94
|
+
*
|
|
95
|
+
* The decision tree is:
|
|
96
|
+
* - no auto-resume flag → 'disabled'
|
|
97
|
+
* - flag set, no session ID known → 'no_session_id'
|
|
98
|
+
* - flag set, session id known, no usable context-stat data → 'no_context_data'
|
|
99
|
+
* (fall back to a fresh run because available headroom cannot be verified)
|
|
100
|
+
* - flag set, peak >= threshold → 'context_too_full'
|
|
101
|
+
* - flag set, peak < threshold → 'ok'
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} params
|
|
104
|
+
* @param {Object} params.argv - parsed CLI arguments
|
|
105
|
+
* @param {string|null} params.sessionId - the session ID to resume, if any
|
|
106
|
+
* @param {Object|null} params.tokenUsage - result of calculateSessionTokens (may be null)
|
|
107
|
+
* @returns {{resume: boolean, reason: string, threshold: number, usedPercent: number|null, peak: number|null, limit: number|null}}
|
|
108
|
+
*/
|
|
109
|
+
export const decideAutoResumeOnUncommittedChanges = ({ argv = {}, sessionId = null, tokenUsage = null } = {}) => {
|
|
110
|
+
const threshold = getAutoResumeMaxContextUsage(argv);
|
|
111
|
+
if (!isAutoResumeOnUncommittedChangesEnabled(argv)) {
|
|
112
|
+
return { resume: false, reason: 'disabled', threshold, usedPercent: null, peak: null, limit: null };
|
|
113
|
+
}
|
|
114
|
+
if (!sessionId) {
|
|
115
|
+
return { resume: false, reason: 'no_session_id', threshold, usedPercent: null, peak: null, limit: null };
|
|
116
|
+
}
|
|
117
|
+
const worst = pickWorstContextUtilisation(tokenUsage, argv);
|
|
118
|
+
if (!worst) {
|
|
119
|
+
return { resume: false, reason: 'no_context_data', threshold, usedPercent: null, peak: null, limit: null };
|
|
120
|
+
}
|
|
121
|
+
const usedPercent = (worst.peak / worst.limit) * 100;
|
|
122
|
+
if (usedPercent >= threshold) {
|
|
123
|
+
return {
|
|
124
|
+
resume: false,
|
|
125
|
+
reason: 'context_too_full',
|
|
126
|
+
threshold,
|
|
127
|
+
usedPercent,
|
|
128
|
+
peak: worst.peak,
|
|
129
|
+
limit: worst.limit,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
resume: true,
|
|
134
|
+
reason: 'ok',
|
|
135
|
+
threshold,
|
|
136
|
+
usedPercent,
|
|
137
|
+
peak: worst.peak,
|
|
138
|
+
limit: worst.limit,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default {
|
|
143
|
+
DEFAULT_MAX_CONTEXT_USAGE_PERCENT,
|
|
144
|
+
getAutoResumeMaxContextUsage,
|
|
145
|
+
isAutoResumeOnUncommittedChangesEnabled,
|
|
146
|
+
pickWorstContextUtilisation,
|
|
147
|
+
decideAutoResumeOnUncommittedChanges,
|
|
148
|
+
};
|
package/src/claude.lib.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
|
17
17
|
import Decimal from 'decimal.js-light';
|
|
18
18
|
import { createEmptySubSessionUsage, accumulateModelUsage, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage, getRawRequestInputTokens, displaySessionTokenUsage } from './claude.budget-stats.lib.mjs';
|
|
19
19
|
import { buildClaudeResumeCommand, buildClaudeAutonomousResumeCommand } from './claude.command-builder.lib.mjs';
|
|
20
|
-
import { seedCumulativeAnthropicCost, addAnthropicRunCost } from './anthropic-cost-accumulator.lib.mjs'; //
|
|
20
|
+
import { beginAnthropicCostScope, seedCumulativeAnthropicCost, addAnthropicRunCost } from './anthropic-cost-accumulator.lib.mjs'; // Issues #1886, #2056
|
|
21
21
|
import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Issue #942
|
|
22
22
|
import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments.lib.mjs'; // Issue #1625
|
|
23
23
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
@@ -236,6 +236,8 @@ export const resolveThinkingSettings = async (argv, log) => {
|
|
|
236
236
|
export const checkPlaywrightMcpAvailability = ensureClaudePlaywrightMcpServer;
|
|
237
237
|
export const executeClaude = async params => {
|
|
238
238
|
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
|
|
239
|
+
// Issue #2056: reset fresh sessions while retaining issue #1886's true-resume accumulation.
|
|
240
|
+
beginAnthropicCostScope({ resume: argv.resume, previousAnthropicCost: argv.previousAnthropicCost });
|
|
239
241
|
if (argv.promptSubagentsViaAgentCommander) {
|
|
240
242
|
try {
|
|
241
243
|
await $`which start-agent`;
|
|
@@ -304,11 +306,9 @@ export const executeClaude = async params => {
|
|
|
304
306
|
}
|
|
305
307
|
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
306
308
|
const escapedSystemPrompt = systemPrompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
307
|
-
|
|
308
309
|
// Issue #1877: deploy the experimental HANDOFF.md Agent Skill so Claude loads
|
|
309
310
|
// it natively from .claude/skills/handoff/SKILL.md (no-op unless --use-handoff).
|
|
310
311
|
await deployHandoffSkill({ tempDir, argv, log, $ });
|
|
311
|
-
|
|
312
312
|
return await withAgentsMdAsClaudeMd({ tempDir, branchName, argv, prompt, fs, path, $, log, formatAligned }, () =>
|
|
313
313
|
executeClaudeCommand({
|
|
314
314
|
tempDir,
|
|
@@ -1176,6 +1176,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1176
1176
|
// logs the failure and returns false; we fall through to the normal commandFailed return below
|
|
1177
1177
|
// (the 400 is not a transient pattern, so it is not retried).
|
|
1178
1178
|
if (commandFailed && retryableLastError.requiresFreshSession && (await tryThinkingBlockRecovery({ classified: retryableLastError, source: 'result', sessionId }))) {
|
|
1179
|
+
beginAnthropicCostScope({ resume: argv.resume, previousAnthropicCost: argv.previousAnthropicCost });
|
|
1179
1180
|
return await executeWithRetry();
|
|
1180
1181
|
}
|
|
1181
1182
|
// Issues #1331, #1353, #1472/#1475: Unified transient error retry (exponential backoff, session preservation)
|
|
@@ -1376,6 +1377,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1376
1377
|
// Issue #1834: Corrupted extended-thinking blocks surfaced as a thrown exception. Same recovery
|
|
1377
1378
|
// as the streamed-result path: resume the session first, then fall back to a fresh restart.
|
|
1378
1379
|
if (retryableException.requiresFreshSession && (await tryThinkingBlockRecovery({ classified: retryableException, source: 'exception', sessionId }))) {
|
|
1380
|
+
beginAnthropicCostScope({ resume: argv.resume, previousAnthropicCost: argv.previousAnthropicCost });
|
|
1379
1381
|
retryCount++;
|
|
1380
1382
|
return await executeWithRetry();
|
|
1381
1383
|
}
|
|
@@ -197,6 +197,8 @@ const KNOWN_OPTION_NAMES = [
|
|
|
197
197
|
'auto-pull-request-creation',
|
|
198
198
|
'auto-commit-uncommitted-changes',
|
|
199
199
|
'auto-restart-on-uncommitted-changes',
|
|
200
|
+
'auto-resume-on-uncommitted-changes',
|
|
201
|
+
'auto-resume-on-uncommitted-changes-maximum-context-window-usage',
|
|
200
202
|
'continue-only-on-feedback',
|
|
201
203
|
'claude-file',
|
|
202
204
|
'gitkeep-file',
|
|
@@ -181,7 +181,11 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
181
181
|
// cost folded in at the runClaude return, plus anything carried from prior
|
|
182
182
|
// iterations via --previous-anthropic-cost.
|
|
183
183
|
const carriedAnthropicCost = getCumulativeAnthropicCost();
|
|
184
|
-
|
|
184
|
+
// Issue #2056: only a real resume reads the same transcript and belongs to
|
|
185
|
+
// the same cost scope. A configured auto-restart deliberately starts a
|
|
186
|
+
// fresh Claude session, so carrying the old cost would recreate the exact
|
|
187
|
+
// cross-session overcount reported in the issue.
|
|
188
|
+
if (!isRestart && carriedAnthropicCost > 0) {
|
|
185
189
|
resumeArgs.push('--previous-anthropic-cost', String(carriedAnthropicCost));
|
|
186
190
|
await log(`💰 Carrying forward cumulative Anthropic cost: $${carriedAnthropicCost.toFixed(6)} (issue #1886)`, { verbose: true });
|
|
187
191
|
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -201,6 +201,16 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
201
201
|
description: 'Automatically restart when uncommitted changes are detected to allow the tool to handle them (default: true, use --no-auto-restart-on-uncommitted-changes to disable)',
|
|
202
202
|
default: true,
|
|
203
203
|
},
|
|
204
|
+
'auto-resume-on-uncommitted-changes': {
|
|
205
|
+
type: 'boolean',
|
|
206
|
+
description: 'EXPERIMENTAL: Automatically resume the previous Claude session when uncommitted changes are detected. Falls back to a fresh session when usable context headroom cannot be verified or is too low. Disabled by default; use --no-auto-resume-on-uncommitted-changes to switch it off explicitly.',
|
|
207
|
+
default: false,
|
|
208
|
+
},
|
|
209
|
+
'auto-resume-on-uncommitted-changes-maximum-context-window-usage': {
|
|
210
|
+
type: 'number',
|
|
211
|
+
description: 'Maximum usable pre-compaction context usage (percent) that still allows --auto-resume-on-uncommitted-changes to resume. The usable limit respects --sub-session-size. At or above this threshold the tool starts a fresh session (default: 50).',
|
|
212
|
+
default: 50,
|
|
213
|
+
},
|
|
204
214
|
'auto-restart-max-iterations': {
|
|
205
215
|
type: 'number',
|
|
206
216
|
description: 'Maximum number of auto-restart iterations before stopping (default: 5, 0 = unlimited)',
|
package/src/solve.mjs
CHANGED
|
@@ -12,7 +12,6 @@ const { configureGitHubRateLimitLogging, wrapDollarWithGhRetry } = await import(
|
|
|
12
12
|
const $ = wrapDollarWithGhRetry(__rawDollar$);
|
|
13
13
|
const config = await import('./solve.config.lib.mjs');
|
|
14
14
|
const { initializeConfig, parseArguments } = config;
|
|
15
|
-
// Import Sentry integration
|
|
16
15
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
17
16
|
const { initializeSentry, addBreadcrumb, reportError, closeSentry } = sentryLib;
|
|
18
17
|
const { yargs, hideBin } = await initializeConfig(use);
|
|
@@ -1334,6 +1333,8 @@ try {
|
|
|
1334
1333
|
prBranch,
|
|
1335
1334
|
branchName,
|
|
1336
1335
|
tempDir,
|
|
1336
|
+
initialSessionId: sessionId,
|
|
1337
|
+
initialResultModelUsage: resultModelUsage,
|
|
1337
1338
|
argv: {
|
|
1338
1339
|
...argv,
|
|
1339
1340
|
watch: argv.watch || shouldRestart, // Enable watch if uncommitted changes
|
|
@@ -1341,7 +1342,6 @@ try {
|
|
|
1341
1342
|
},
|
|
1342
1343
|
});
|
|
1343
1344
|
|
|
1344
|
-
// Update session data with latest from watch mode for accurate pricing
|
|
1345
1345
|
if (watchResult && watchResult.latestSessionId) {
|
|
1346
1346
|
sessionId = watchResult.latestSessionId;
|
|
1347
1347
|
anthropicTotalCostUSD = watchResult.latestAnthropicCost;
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -65,6 +65,11 @@ const { trackAuthenticatedUserCommentsSince } = autoMergeHelpers;
|
|
|
65
65
|
const resultsLib = await import('./solve.results.lib.mjs');
|
|
66
66
|
const { maybeAttachWorkingSessionSummary, ensurePullRequestIssueLink } = resultsLib;
|
|
67
67
|
|
|
68
|
+
// Issue #1056: Auto-resume on uncommitted changes — decide whether to call the
|
|
69
|
+
// agent again with --resume <sessionId> instead of restarting from scratch.
|
|
70
|
+
const autoResumeLib = await import('./auto-resume-uncommitted.lib.mjs');
|
|
71
|
+
const { decideAutoResumeOnUncommittedChanges, isAutoResumeOnUncommittedChangesEnabled } = autoResumeLib;
|
|
72
|
+
|
|
68
73
|
/**
|
|
69
74
|
* Monitor for feedback in a loop and trigger restart when detected
|
|
70
75
|
*/
|
|
@@ -76,8 +81,12 @@ export const watchForFeedback = async params => {
|
|
|
76
81
|
const maxAutoRestartIterations = normalizeAutoIterationLimit(argv.autoRestartMaxIterations);
|
|
77
82
|
|
|
78
83
|
// Track latest session data across all iterations for accurate pricing
|
|
79
|
-
|
|
84
|
+
// Issue #1056: Seed from the *initial* tool execution so the first auto-restart
|
|
85
|
+
// iteration can attempt --resume on the original session if the user opted in
|
|
86
|
+
// via --auto-resume-on-uncommitted-changes.
|
|
87
|
+
let latestSessionId = params.initialSessionId || null;
|
|
80
88
|
let latestAnthropicCost = null;
|
|
89
|
+
let latestResultModelUsage = params.initialResultModelUsage || null;
|
|
81
90
|
|
|
82
91
|
// Issue #1290: Track whether auto-restart iterations actually ran and whether logs were uploaded
|
|
83
92
|
// This helps solve.mjs decide whether to upload final logs
|
|
@@ -323,16 +332,43 @@ export const watchForFeedback = async params => {
|
|
|
323
332
|
|
|
324
333
|
let restartFeedbackLines = feedbackLines;
|
|
325
334
|
let restartArgv = argv;
|
|
326
|
-
const
|
|
335
|
+
const isUncommittedChangesRestart = isTemporaryWatch && (firstIterationInTemporaryMode || hasUncommittedInTempMode);
|
|
336
|
+
const isClaudeTool = argv.tool === 'claude' || !argv.tool;
|
|
337
|
+
const autoResumeOnUncommittedChanges = isUncommittedChangesRestart && isClaudeTool && isAutoResumeOnUncommittedChangesEnabled(argv);
|
|
338
|
+
let autoResumeDecision = null;
|
|
339
|
+
|
|
340
|
+
if (autoResumeOnUncommittedChanges) {
|
|
341
|
+
let tokenUsage = null;
|
|
342
|
+
if (latestSessionId && tempDir) {
|
|
343
|
+
try {
|
|
344
|
+
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
345
|
+
tokenUsage = await calculateSessionTokens(latestSessionId, tempDir, latestResultModelUsage);
|
|
346
|
+
} catch (tokenError) {
|
|
347
|
+
await log(` ⚠️ Could not calculate token usage for auto-resume decision: ${tokenError.message}`, { verbose: true });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
autoResumeDecision = decideAutoResumeOnUncommittedChanges({ argv, sessionId: latestSessionId, tokenUsage });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Keep the older issue #661 experiment working independently. The issue
|
|
354
|
+
// #1056 option takes precedence when enabled because it adds the required
|
|
355
|
+
// context-headroom safety check.
|
|
356
|
+
const legacyResumeRequested = isUncommittedChangesRestart && isClaudeTool && (argv.resumeOnAutoRestart || argv['resume-on-auto-restart']);
|
|
357
|
+
const shouldUseSessionResume = autoResumeOnUncommittedChanges ? autoResumeDecision?.resume === true : Boolean(legacyResumeRequested && global.previousSessionId);
|
|
358
|
+
const resumeSessionId = autoResumeOnUncommittedChanges ? latestSessionId : global.previousSessionId;
|
|
327
359
|
|
|
328
360
|
if (shouldUseSessionResume) {
|
|
329
361
|
await log(formatAligned('', 'Experimental session resume: using minimal auto-restart prompt', '', 2));
|
|
330
|
-
await log(formatAligned('', `Resuming session: ${
|
|
362
|
+
await log(formatAligned('', `Resuming session: ${resumeSessionId}`, '', 2));
|
|
363
|
+
|
|
364
|
+
if (autoResumeDecision?.reason === 'ok') {
|
|
365
|
+
await log(formatAligned('', `Peak context usage: ${autoResumeDecision.peak.toLocaleString()} / ${autoResumeDecision.limit.toLocaleString()} usable tokens (${autoResumeDecision.usedPercent.toFixed(1)}%, threshold ${autoResumeDecision.threshold}%)`, '', 2));
|
|
366
|
+
}
|
|
331
367
|
|
|
332
|
-
if (argv.verbose) {
|
|
368
|
+
if (!autoResumeOnUncommittedChanges && argv.verbose) {
|
|
333
369
|
try {
|
|
334
370
|
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
335
|
-
const tokenUsage = await calculateSessionTokens(
|
|
371
|
+
const tokenUsage = await calculateSessionTokens(resumeSessionId, tempDir);
|
|
336
372
|
if (tokenUsage?.totalTokens) {
|
|
337
373
|
await log(formatAligned('', `Previous session tokens: ${tokenUsage.totalTokens.toLocaleString()}`, '', 2));
|
|
338
374
|
}
|
|
@@ -346,11 +382,14 @@ export const watchForFeedback = async params => {
|
|
|
346
382
|
restartFeedbackLines = [minimalPrompt];
|
|
347
383
|
restartArgv = {
|
|
348
384
|
...argv,
|
|
349
|
-
resume:
|
|
385
|
+
resume: resumeSessionId,
|
|
350
386
|
minimalRestartContext: true,
|
|
351
387
|
};
|
|
352
388
|
|
|
353
389
|
await log(formatAligned('', `Minimal restart prompt size: ${minimalPrompt.length} characters`, '', 2));
|
|
390
|
+
} else if (autoResumeOnUncommittedChanges) {
|
|
391
|
+
const skipReason = autoResumeDecision?.reason === 'context_too_full' ? `peak context usage ${autoResumeDecision.usedPercent.toFixed(1)}% reached the ${autoResumeDecision.threshold}% threshold` : autoResumeDecision?.reason === 'no_session_id' ? 'no previous session ID is available' : 'context usage could not be verified';
|
|
392
|
+
await log(formatAligned('', 'Auto-resume skipped:', `${skipReason}; starting a fresh session`, 2));
|
|
354
393
|
}
|
|
355
394
|
|
|
356
395
|
// Execute tool using shared utility
|
|
@@ -498,6 +537,12 @@ export const watchForFeedback = async params => {
|
|
|
498
537
|
}
|
|
499
538
|
}
|
|
500
539
|
|
|
540
|
+
// Issue #1056: Track latest model usage so the next auto-resume decision
|
|
541
|
+
// can re-evaluate context-window usage against the most recent peak.
|
|
542
|
+
if (toolResult.resultModelUsage) {
|
|
543
|
+
latestResultModelUsage = toolResult.resultModelUsage;
|
|
544
|
+
}
|
|
545
|
+
|
|
501
546
|
// Issue #1508: Compute budget stats for auto-restart log comment
|
|
502
547
|
let autoRestartBudgetStatsData = null;
|
|
503
548
|
if (argv.tokensBudgetStats && latestSessionId && tempDir) {
|