mindweave 2.5.1 → 3.0.1
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/README.md +50 -114
- package/dist/cli/App.js +580 -305
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/analytics.js +27 -18
- package/dist/cli/analytics.js.map +1 -1
- package/dist/cli/attachments.js +34 -0
- package/dist/cli/attachments.js.map +1 -1
- package/dist/cli/bannerFit.js +70 -0
- package/dist/cli/bannerFit.js.map +1 -0
- package/dist/cli/bootstrap.js +41 -3
- package/dist/cli/bootstrap.js.map +1 -1
- package/dist/cli/commands.js +4 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/components/ApprovalBox.js +3 -2
- package/dist/cli/components/ApprovalBox.js.map +1 -1
- package/dist/cli/components/BlockView.js +27 -6
- package/dist/cli/components/BlockView.js.map +1 -1
- package/dist/cli/components/FirstRunFrame.js +2 -1
- package/dist/cli/components/FirstRunFrame.js.map +1 -1
- package/dist/cli/components/KeyManager.js +5 -4
- package/dist/cli/components/KeyManager.js.map +1 -1
- package/dist/cli/components/KeySetup.js +29 -13
- package/dist/cli/components/KeySetup.js.map +1 -1
- package/dist/cli/components/MarathonBox.js +64 -0
- package/dist/cli/components/MarathonBox.js.map +1 -0
- package/dist/cli/components/McpMinitabs.js +8 -7
- package/dist/cli/components/McpMinitabs.js.map +1 -1
- package/dist/cli/components/Picker.js +4 -3
- package/dist/cli/components/Picker.js.map +1 -1
- package/dist/cli/components/PromptInput.js +47 -8
- package/dist/cli/components/PromptInput.js.map +1 -1
- package/dist/cli/components/PulseDot.js +42 -0
- package/dist/cli/components/PulseDot.js.map +1 -0
- package/dist/cli/components/SubagentView.js +9 -6
- package/dist/cli/components/SubagentView.js.map +1 -1
- package/dist/cli/components/TipLine.js +2 -1
- package/dist/cli/components/TipLine.js.map +1 -1
- package/dist/cli/components/ToolGroup.js +65 -49
- package/dist/cli/components/ToolGroup.js.map +1 -1
- package/dist/cli/components/ToolLine.js +20 -19
- package/dist/cli/components/ToolLine.js.map +1 -1
- package/dist/cli/components/TrustGate.js +2 -1
- package/dist/cli/components/TrustGate.js.map +1 -1
- package/dist/cli/components/minitabs.js +3 -2
- package/dist/cli/components/minitabs.js.map +1 -1
- package/dist/cli/desktopApp.js +45 -0
- package/dist/cli/desktopApp.js.map +1 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inkOutputPatch.js +285 -0
- package/dist/cli/inkOutputPatch.js.map +1 -0
- package/dist/cli/inkSpeed.js +29 -0
- package/dist/cli/inkSpeed.js.map +1 -0
- package/dist/cli/inkSpeedLoader.js +47 -0
- package/dist/cli/inkSpeedLoader.js.map +1 -0
- package/dist/cli/inputText.js +30 -0
- package/dist/cli/inputText.js.map +1 -0
- package/dist/cli/keyManager.js +2 -1
- package/dist/cli/keyManager.js.map +1 -1
- package/dist/cli/keyPrefs.js +90 -0
- package/dist/cli/keyPrefs.js.map +1 -0
- package/dist/cli/keySetup.js +5 -2
- package/dist/cli/keySetup.js.map +1 -1
- package/dist/cli/marathonUi.js +113 -0
- package/dist/cli/marathonUi.js.map +1 -0
- package/dist/cli/markdown.js +63 -11
- package/dist/cli/markdown.js.map +1 -1
- package/dist/cli/modes.js +3 -3
- package/dist/cli/modes.js.map +1 -1
- package/dist/cli/prodEnv.js +43 -0
- package/dist/cli/prodEnv.js.map +1 -0
- package/dist/cli/revealPace.js +5 -5
- package/dist/cli/revealPace.js.map +1 -1
- package/dist/cli/revealQueue.js +120 -0
- package/dist/cli/revealQueue.js.map +1 -0
- package/dist/cli/startupArgs.js +1 -1
- package/dist/cli/theme.js +47 -0
- package/dist/cli/theme.js.map +1 -0
- package/dist/cli/toolDisplay.js +112 -18
- package/dist/cli/toolDisplay.js.map +1 -1
- package/dist/cli/transcript.js +1 -1
- package/dist/cli/transcript.js.map +1 -1
- package/dist/cli/virtualWindow.js +14 -2
- package/dist/cli/virtualWindow.js.map +1 -1
- package/dist/cli/wheelAccel.js +73 -0
- package/dist/cli/wheelAccel.js.map +1 -0
- package/dist/cli/widestLinePatch.js +48 -0
- package/dist/cli/widestLinePatch.js.map +1 -0
- package/dist/cli/workingVerb.js +88 -28
- package/dist/cli/workingVerb.js.map +1 -1
- package/dist/cli/wrap.js +1 -1
- package/dist/cli/wrap.js.map +1 -1
- package/dist/core/contextSettings.js +84 -0
- package/dist/core/contextSettings.js.map +1 -0
- package/dist/core/limitsView.js +40 -0
- package/dist/core/limitsView.js.map +1 -0
- package/dist/core/mcpServers.js +164 -0
- package/dist/core/mcpServers.js.map +1 -0
- package/dist/core/permissions.js +209 -0
- package/dist/core/permissions.js.map +1 -0
- package/dist/core/providerKeys.js +187 -0
- package/dist/core/providerKeys.js.map +1 -0
- package/dist/core/rulesSkills.js +142 -0
- package/dist/core/rulesSkills.js.map +1 -0
- package/dist/core/spendView.js +90 -0
- package/dist/core/spendView.js.map +1 -0
- package/dist/core/turnRunner.js +453 -60
- package/dist/core/turnRunner.js.map +1 -1
- package/dist/drivers/anthropic/client.js +24 -5
- package/dist/drivers/anthropic/client.js.map +1 -1
- package/dist/drivers/anthropic/manifest.js +15 -2
- package/dist/drivers/anthropic/manifest.js.map +1 -1
- package/dist/drivers/cerebras/manifest.js +6 -2
- package/dist/drivers/cerebras/manifest.js.map +1 -1
- package/dist/drivers/deepseek/manifest.js +12 -3
- package/dist/drivers/deepseek/manifest.js.map +1 -1
- package/dist/drivers/glm/manifest.js +3 -1
- package/dist/drivers/glm/manifest.js.map +1 -1
- package/dist/drivers/groq/manifest.js +5 -2
- package/dist/drivers/groq/manifest.js.map +1 -1
- package/dist/drivers/ollama/catalog.js +71 -0
- package/dist/drivers/ollama/catalog.js.map +1 -0
- package/dist/drivers/ollama/client.js +164 -0
- package/dist/drivers/ollama/client.js.map +1 -0
- package/dist/drivers/ollama/endpoint.js +42 -0
- package/dist/drivers/ollama/endpoint.js.map +1 -0
- package/dist/drivers/ollama/index.js +5 -0
- package/dist/drivers/ollama/index.js.map +1 -0
- package/dist/drivers/ollama/manifest.js +64 -0
- package/dist/drivers/ollama/manifest.js.map +1 -0
- package/dist/drivers/openai/client.js +10 -4
- package/dist/drivers/openai/client.js.map +1 -1
- package/dist/drivers/openai/manifest.js +46 -12
- package/dist/drivers/openai/manifest.js.map +1 -1
- package/dist/drivers/registry.js +11 -0
- package/dist/drivers/registry.js.map +1 -1
- package/dist/drivers/retryPolicy.js +26 -1
- package/dist/drivers/retryPolicy.js.map +1 -1
- package/dist/dynamo/auxModel.js +54 -0
- package/dist/dynamo/auxModel.js.map +1 -0
- package/dist/dynamo/engine.js +380 -99
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/dynamo/guard.js +10 -0
- package/dist/dynamo/guard.js.map +1 -1
- package/dist/dynamo/liveMeter.js +25 -6
- package/dist/dynamo/liveMeter.js.map +1 -1
- package/dist/dynamo/marathon.js +409 -0
- package/dist/dynamo/marathon.js.map +1 -0
- package/dist/dynamo/marathonPrompt.js +45 -0
- package/dist/dynamo/marathonPrompt.js.map +1 -0
- package/dist/dynamo/model.js +6 -2
- package/dist/dynamo/model.js.map +1 -1
- package/dist/dynamo/toolBatches.js +58 -0
- package/dist/dynamo/toolBatches.js.map +1 -0
- package/dist/dynamo/usageLimits.js +446 -0
- package/dist/dynamo/usageLimits.js.map +1 -0
- package/dist/governor/freshness.js +10 -7
- package/dist/governor/freshness.js.map +1 -1
- package/dist/governor/index.js +76 -9
- package/dist/governor/index.js.map +1 -1
- package/dist/governor/write.js +43 -14
- package/dist/governor/write.js.map +1 -1
- package/dist/index.js +18 -151
- package/dist/index.js.map +1 -1
- package/dist/main.js +162 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/manager.js +18 -0
- package/dist/mcp/manager.js.map +1 -1
- package/dist/memory/allMetas.js +69 -0
- package/dist/memory/allMetas.js.map +1 -0
- package/dist/memory/compaction.js +69 -14
- package/dist/memory/compaction.js.map +1 -1
- package/dist/memory/earlier.js +15 -0
- package/dist/memory/earlier.js.map +1 -0
- package/dist/memory/profile.js +93 -0
- package/dist/memory/profile.js.map +1 -0
- package/dist/memory/rewind.js +262 -0
- package/dist/memory/rewind.js.map +1 -0
- package/dist/memory/session.js +42 -10
- package/dist/memory/session.js.map +1 -1
- package/dist/memory/sessionMemory.js +48 -13
- package/dist/memory/sessionMemory.js.map +1 -1
- package/dist/memory/sessionMemoryCompact.js +1 -1
- package/dist/memory/sessionMemoryCompact.js.map +1 -1
- package/dist/memory/sessionTitle.js +50 -0
- package/dist/memory/sessionTitle.js.map +1 -0
- package/dist/memory/store.js +42 -1
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/tools/askUser.js +14 -0
- package/dist/tools/askUser.js.map +1 -1
- package/dist/tools/backgroundShells.js +208 -5
- package/dist/tools/backgroundShells.js.map +1 -1
- package/dist/tools/browser.js +176 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/cdp.js +165 -0
- package/dist/tools/cdp.js.map +1 -0
- package/dist/tools/checkpoints.js +208 -5
- package/dist/tools/checkpoints.js.map +1 -1
- package/dist/tools/deferredNative.js +2 -0
- package/dist/tools/deferredNative.js.map +1 -1
- package/dist/tools/exitPlan.js +4 -0
- package/dist/tools/exitPlan.js.map +1 -1
- package/dist/tools/hiddenDesktop.js +273 -0
- package/dist/tools/hiddenDesktop.js.map +1 -0
- package/dist/tools/listeningPorts.js +134 -0
- package/dist/tools/listeningPorts.js.map +1 -0
- package/dist/tools/mcpSearch.js +8 -0
- package/dist/tools/mcpSearch.js.map +1 -1
- package/dist/tools/mindweaveStatus.js +1 -1
- package/dist/tools/mindweaveStatus.js.map +1 -1
- package/dist/tools/registry.js +3 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/runCommand.js +107 -6
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/screenshot.js +4 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/screenshotWin.js +26 -9
- package/dist/tools/screenshotWin.js.map +1 -1
- package/dist/tools/stateCheckpoint.js +128 -0
- package/dist/tools/stateCheckpoint.js.map +1 -0
- package/dist/tools/todo.js +6 -0
- package/dist/tools/todo.js.map +1 -1
- package/dist/tools/ui.js +1295 -0
- package/dist/tools/ui.js.map +1 -0
- package/dist/tools/uiHide.js +356 -0
- package/dist/tools/uiHide.js.map +1 -0
- package/dist/tools/uiPage.js +861 -0
- package/dist/tools/uiPage.js.map +1 -0
- package/dist/tools/uiWin.js +506 -0
- package/dist/tools/uiWin.js.map +1 -0
- package/dist/tools/webFetch.js +32 -25
- package/dist/tools/webFetch.js.map +1 -1
- package/dist/tools/webSearch.js +12 -0
- package/dist/tools/webSearch.js.map +1 -1
- package/package.json +78 -78
package/dist/dynamo/engine.js
CHANGED
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
* the read-only tool exactly like any other tool call — so this whole function
|
|
14
14
|
* can later move to a server unchanged, with tools executing on the client.
|
|
15
15
|
*/
|
|
16
|
+
import { readProfile, profilePrompt } from "../memory/profile.js";
|
|
17
|
+
import { narrationShown, noteReads } from "../cli/toolDisplay.js";
|
|
16
18
|
import { activeDriver, ensureDriver, manifestForModel } from "../drivers/registry.js";
|
|
17
19
|
import { summarizeTask, taskLimitReason } from "./pricing.js";
|
|
20
|
+
import { limitGateReason, noteUsage, refreshUsageLimits, takeLimitWarnings } from "./usageLimits.js";
|
|
18
21
|
import { addTurn, emptySpend } from "./spend.js";
|
|
19
22
|
import { mutationNeedsVerification, isVerification, reScopeCheck, isBackgroundPollStep, stepFailureSignature, repeatFailureStep, repeatFailureNudge, failedActionLabel, firstErrorLine, sameFileEditCounts, overusedSingleEdits, batchEditNudge, narrationFault, narrationNudge, unknownToolError, replyFault, replyRewrite, VERIFY_NUDGE } from "./verify.js";
|
|
20
23
|
import { guardOptions, GUARD_REFUSAL, GUARD_REFUSAL_INPUT, guardRefusalWith, guardQuestion, guardDetail, interpretGuardChoice } from "./guard.js";
|
|
@@ -23,19 +26,24 @@ import { findTool, toolSchemas, TOOLS } from "../tools/registry.js";
|
|
|
23
26
|
import { deferredToolsIndex } from "../tools/deferredNative.js";
|
|
24
27
|
import { prefixPrint, diffPrefix, cacheCallLine, writeCacheLog } from "./cacheBreak.js";
|
|
25
28
|
import { commandShellLabel } from "../tools/runCommand.js";
|
|
29
|
+
import { uiLiveState } from "../tools/ui.js";
|
|
30
|
+
import { isAgenticOnlyRefusal, withAuxModel } from "./auxModel.js";
|
|
31
|
+
import { marathonBlock } from "./marathonPrompt.js";
|
|
26
32
|
import { basePrompt } from "./prompt.js";
|
|
27
33
|
import { basename } from "node:path";
|
|
28
34
|
import { randomUUID } from "node:crypto";
|
|
29
35
|
import { promises as fsp } from "node:fs";
|
|
30
36
|
import { relativize, resolvePath, rootLabel, rootsOf } from "../tools/paths.js";
|
|
31
37
|
import { renderRules, renderSkillCatalog, reloadGovernance, governanceStamp, rescope } from "../governor/index.js";
|
|
32
|
-
import {
|
|
38
|
+
import { writesAgentState, snapshotAgentState, recordAgentStateChanges } from "../tools/stateCheckpoint.js";
|
|
39
|
+
import { forkSession, reconcileInterruptedTools, reloadProjectMemory, repairToolCallOrder } from "../memory/session.js";
|
|
33
40
|
import { selectActiveFiles } from "../memory/workingSet.js";
|
|
34
41
|
import { directoryNotesFor } from "../memory/projectNotes.js";
|
|
35
42
|
import { rippleCheck } from "../tools/editRipple.js";
|
|
36
43
|
import { fullReadPaths } from "../memory/presence.js";
|
|
37
44
|
import { RESTORE_MAX_TOKENS_PER_FILE, renderRestored, restoreBudgetFor, selectForRestore, } from "../memory/restore.js";
|
|
38
|
-
import { KEEP_LAST_N, KEEP_LAST_N_BOUNDARY, summaryRequest, SUMMARY_SYSTEM_PROMPT, dropOldestRounds, estimateEntriesTokens, estimateTokens, estimateTokensForChars, formatTranscriptForSummary, isContinuation, microcompact, spliceSummary, usableSummary, } from "../memory/compaction.js";
|
|
45
|
+
import { KEEP_LAST_N, KEEP_LAST_N_BOUNDARY, summaryRequest, SUMMARY_SYSTEM_PROMPT, dropOldestRounds, estimateEntriesTokens, estimateTokens, estimateTokensForChars, formatTranscriptForSummary, isContinuation, microcompact, spliceSummary, summaryForWire, summaryIsHistory, usableSummary, } from "../memory/compaction.js";
|
|
46
|
+
import { MAX_PARALLEL_CALLS, partitionCalls, runLimited } from "./toolBatches.js";
|
|
39
47
|
import { loadPlanArtifact, completePlanArtifact, renderPlanBlock, planDivergenceStop } from "./planArtifact.js";
|
|
40
48
|
import { autoCompactThreshold, microCompactThreshold, cacheLikelyCold, clearIsWorthIt, measuredOverhead, sharpContextWindow, } from "./contextWindow.js";
|
|
41
49
|
import { renderSessionMemory, shouldUpdateSessionMemory, updateSessionMemory } from "../memory/sessionMemory.js";
|
|
@@ -43,6 +51,7 @@ import { compactFromSessionMemory } from "../memory/sessionMemoryCompact.js";
|
|
|
43
51
|
import { isContextOverflowError } from "../drivers/contextOverflow.js";
|
|
44
52
|
import { detailOf, providerMessage } from "../drivers/providerError.js";
|
|
45
53
|
import { transcriptPath } from "../memory/store.js";
|
|
54
|
+
import { replaceTranscript } from "../memory/earlier.js";
|
|
46
55
|
/** Stop retrying autocompact after this many consecutive failures in a session, so a
|
|
47
56
|
* transcript that's irrecoverably over the limit can't hammer the summarizer each turn
|
|
48
57
|
* (a circuit-breaker for runaway retry loops, which can otherwise pile up thousands of doomed retries). */
|
|
@@ -55,14 +64,19 @@ const MAX_COMPACT_FAILURES = 3;
|
|
|
55
64
|
// line we hold is the thin-prompt boundary: rich on what the harness owns, but
|
|
56
65
|
// we still do NOT teach engineering judgment (how to debug, how to write code) —
|
|
57
66
|
// that is the model's job.
|
|
58
|
-
export function staticSystemPrompt(projectContext, projectMemory, memoryDir, memoryIndex, governance, workspace, priorSessions = 0) {
|
|
67
|
+
export function staticSystemPrompt(projectContext, projectMemory, memoryDir, memoryIndex, governance, workspace, priorSessions = 0, aboutUser = "") {
|
|
59
68
|
let prompt = basePrompt(commandShellLabel());
|
|
69
|
+
// The user's own profile (memory/profile.ts): name, experience level, reply style.
|
|
70
|
+
if (aboutUser)
|
|
71
|
+
prompt += `
|
|
72
|
+
|
|
73
|
+
${aboutUser}`;
|
|
60
74
|
if (workspace) {
|
|
61
|
-
prompt += `
|
|
62
|
-
|
|
63
|
-
This session spans more than one root folder. Each file is addressed as \`label/path\`; search tools cover every root unless you pass a specific \`path\`. The roots are:
|
|
64
|
-
<workspace>
|
|
65
|
-
${workspace}
|
|
75
|
+
prompt += `
|
|
76
|
+
|
|
77
|
+
This session spans more than one root folder. Each file is addressed as \`label/path\`; search tools cover every root unless you pass a specific \`path\`. The roots are:
|
|
78
|
+
<workspace>
|
|
79
|
+
${workspace}
|
|
66
80
|
</workspace>`;
|
|
67
81
|
}
|
|
68
82
|
// NOTE: the user's standing rules are deliberately NOT rendered here. They live
|
|
@@ -73,41 +87,41 @@ ${workspace}
|
|
|
73
87
|
// skills are a reference catalog), so they alone get the salience boost. Keeping
|
|
74
88
|
// them out of the prefix also stops a mid-session `remember_rule` from busting it.
|
|
75
89
|
if (governance.forbidden) {
|
|
76
|
-
prompt += `
|
|
77
|
-
|
|
78
|
-
You are FORBIDDEN from modifying these paths — never write, edit, or run a command that changes them. The tools also enforce this and will refuse, but do not even try:
|
|
79
|
-
<forbidden>
|
|
80
|
-
${governance.forbidden}
|
|
90
|
+
prompt += `
|
|
91
|
+
|
|
92
|
+
You are FORBIDDEN from modifying these paths — never write, edit, or run a command that changes them. The tools also enforce this and will refuse, but do not even try:
|
|
93
|
+
<forbidden>
|
|
94
|
+
${governance.forbidden}
|
|
81
95
|
</forbidden>`;
|
|
82
96
|
}
|
|
83
97
|
if (governance.forbiddenCommands) {
|
|
84
|
-
prompt += `
|
|
85
|
-
|
|
86
|
-
You are FORBIDDEN from running these commands (or any command that contains one) — run_command will refuse them and only the user can lift that. Do not attempt them or a workaround:
|
|
87
|
-
<forbidden_commands>
|
|
88
|
-
${governance.forbiddenCommands}
|
|
98
|
+
prompt += `
|
|
99
|
+
|
|
100
|
+
You are FORBIDDEN from running these commands (or any command that contains one) — run_command will refuse them and only the user can lift that. Do not attempt them or a workaround:
|
|
101
|
+
<forbidden_commands>
|
|
102
|
+
${governance.forbiddenCommands}
|
|
89
103
|
</forbidden_commands>`;
|
|
90
104
|
}
|
|
91
105
|
if (governance.skills) {
|
|
92
|
-
prompt += `
|
|
93
|
-
|
|
94
|
-
You have project skills available — named procedures you can run. To run one, call use_skill with its name; its full steps are loaded then (you only see the summary here). Use one when its description fits the task:
|
|
95
|
-
<available_skills>
|
|
96
|
-
${governance.skills}
|
|
106
|
+
prompt += `
|
|
107
|
+
|
|
108
|
+
You have project skills available — named procedures you can run. To run one, call use_skill with its name; its full steps are loaded then (you only see the summary here). Use one when its description fits the task:
|
|
109
|
+
<available_skills>
|
|
110
|
+
${governance.skills}
|
|
97
111
|
</available_skills>`;
|
|
98
112
|
}
|
|
99
113
|
if (projectContext) {
|
|
100
|
-
prompt += `
|
|
101
|
-
|
|
102
|
-
The following describes the project and machine you're working in, captured at the start of this session (a snapshot — use tools for anything current or deeper):
|
|
114
|
+
prompt += `
|
|
115
|
+
|
|
116
|
+
The following describes the project and machine you're working in, captured at the start of this session (a snapshot — use tools for anything current or deeper):
|
|
103
117
|
${projectContext}`;
|
|
104
118
|
}
|
|
105
119
|
if (projectMemory) {
|
|
106
|
-
prompt += `
|
|
107
|
-
|
|
108
|
-
The project provides this context in its MINDWEAVE.md — treat it as background facts about this codebase:
|
|
109
|
-
<project_memory>
|
|
110
|
-
${projectMemory}
|
|
120
|
+
prompt += `
|
|
121
|
+
|
|
122
|
+
The project provides this context in its MINDWEAVE.md — treat it as background facts about this codebase:
|
|
123
|
+
<project_memory>
|
|
124
|
+
${projectMemory}
|
|
111
125
|
</project_memory>`;
|
|
112
126
|
}
|
|
113
127
|
// Its own past work in this project. The COUNT goes in the prompt (so the model
|
|
@@ -117,16 +131,16 @@ ${projectMemory}
|
|
|
117
131
|
// and a question about past work gets a real answer instead of a deflection.
|
|
118
132
|
if (priorSessions > 0) {
|
|
119
133
|
const s = priorSessions === 1 ? "" : "s";
|
|
120
|
-
prompt += `
|
|
121
|
-
|
|
134
|
+
prompt += `
|
|
135
|
+
|
|
122
136
|
You have worked in this project before: ${priorSessions} earlier session${s} of yours are saved, and you can read them. When the user refers to earlier work — "last session", "what did we do", "the bug we fixed" — call \`sessions\` to list them, then \`sessions\` again with an id to read the one they mean, and answer from what you find. It is not in your tool list until you load it with find_tools. Do not say you cannot see your past sessions, and do not guess from the project files instead. \`/continue\` is for the user to RESUME a session; it is not a substitute for you looking. Never present another tool's saved conversations as your own.`;
|
|
123
137
|
}
|
|
124
138
|
if (memoryDir) {
|
|
125
|
-
prompt += `
|
|
126
|
-
|
|
127
|
-
Your cross-session memory for this project lives in \`${memoryDir}\` (read or grep the topic files there for the full text of any entry). Its index:
|
|
128
|
-
<memory_index>
|
|
129
|
-
${memoryIndex || "(empty — nothing has been saved to memory yet)"}
|
|
139
|
+
prompt += `
|
|
140
|
+
|
|
141
|
+
Your cross-session memory for this project lives in \`${memoryDir}\` (read or grep the topic files there for the full text of any entry). Its index:
|
|
142
|
+
<memory_index>
|
|
143
|
+
${memoryIndex || "(empty — nothing has been saved to memory yet)"}
|
|
130
144
|
</memory_index>`;
|
|
131
145
|
}
|
|
132
146
|
// The deferred pool's index. Roughly forty tokens standing in for several hundred of
|
|
@@ -134,8 +148,8 @@ ${memoryIndex || "(empty — nothing has been saved to memory yet)"}
|
|
|
134
148
|
// missing feature, and the model routes around a capability it actually has.
|
|
135
149
|
const deferred = deferredToolsIndex();
|
|
136
150
|
if (deferred) {
|
|
137
|
-
prompt += `
|
|
138
|
-
|
|
151
|
+
prompt += `
|
|
152
|
+
|
|
139
153
|
${deferred}`;
|
|
140
154
|
}
|
|
141
155
|
return prompt;
|
|
@@ -158,7 +172,9 @@ export function volatileContext(rules, planMode, sessionMemory, approvedPlan = "
|
|
|
158
172
|
/** Notes for the folders being worked in right now (see memory/projectNotes.ts). */
|
|
159
173
|
directoryNotes = [],
|
|
160
174
|
/** Where the previous turn left the shell, when this turn started back at the root. */
|
|
161
|
-
cwdResetFrom = ""
|
|
175
|
+
cwdResetFrom = "",
|
|
176
|
+
/** A running Marathon's standing block (see marathonPrompt.ts), or "". */
|
|
177
|
+
marathon = "") {
|
|
162
178
|
const parts = [];
|
|
163
179
|
// Each turn starts at the project root, and a model that `cd`-ed into a subfolder last
|
|
164
180
|
// turn does not know that unless it is told. It was not, and a real session ran
|
|
@@ -195,6 +211,10 @@ cwdResetFrom = "") {
|
|
|
195
211
|
"you following that plan. Do not write the plan out as an ordinary reply and stop — prose between steps is " +
|
|
196
212
|
"shortened before the user sees it, so a plan presented that way reaches them in pieces.");
|
|
197
213
|
}
|
|
214
|
+
// A running Marathon is standing instruction like the approved plan: rendered fresh
|
|
215
|
+
// every request so compaction cannot lose what the run is for.
|
|
216
|
+
if (marathon)
|
|
217
|
+
parts.push(marathon);
|
|
198
218
|
// The maintained session state — first in the volatile tail so the model reads
|
|
199
219
|
// "here's where we are" before the map/task list. Survives compaction.
|
|
200
220
|
const memBlock = renderSessionMemory(sessionMemory);
|
|
@@ -482,6 +502,8 @@ async function loadImagePayloads(session) {
|
|
|
482
502
|
}));
|
|
483
503
|
return out;
|
|
484
504
|
}
|
|
505
|
+
/** What the wire carries in place of an assistant reply that had no words (see buildRequest). */
|
|
506
|
+
export const EMPTY_REPLY_PLACEHOLDER = "(no reply)";
|
|
485
507
|
function buildRequest(session, tools, imagePayloads = new Map(),
|
|
486
508
|
/** Notes for the folders in play, resolved by the caller (it has to read disk). */
|
|
487
509
|
directoryNotes = []) {
|
|
@@ -492,7 +514,7 @@ directoryNotes = []) {
|
|
|
492
514
|
? manifestForModel(session.modelConfig.model).imageTypes?.(session.modelConfig.model)
|
|
493
515
|
: undefined;
|
|
494
516
|
const messages = [];
|
|
495
|
-
for (const e of session.transcript) {
|
|
517
|
+
for (const [i, e] of session.transcript.entries()) {
|
|
496
518
|
if (e.role === "user" || e.role === "summary") {
|
|
497
519
|
// Attached images ride with the message, but only while their payload is still
|
|
498
520
|
// live: microcompaction drops the refs once the turn is old, and a file deleted
|
|
@@ -502,7 +524,11 @@ directoryNotes = []) {
|
|
|
502
524
|
// A message that arrived mid-turn is framed HERE rather than being stored framed,
|
|
503
525
|
// so the transcript keeps what the person typed and only the wire carries the
|
|
504
526
|
// explanation. Deterministic from the entry, so the cached prefix is unaffected.
|
|
505
|
-
|
|
527
|
+
// A summary is sent as background once the user has written after it, so its
|
|
528
|
+
// hand-off "next step" cannot keep overriding what they actually asked.
|
|
529
|
+
const said = e.role === "summary"
|
|
530
|
+
? summaryForWire(e.content, summaryIsHistory(e, session.transcript.slice(i + 1)))
|
|
531
|
+
: e.arrival ? arrivalNote(e.arrival, e.content) : e.content;
|
|
506
532
|
if (refs && refs.length > 0) {
|
|
507
533
|
const images = [];
|
|
508
534
|
const missing = [];
|
|
@@ -541,8 +567,8 @@ directoryNotes = []) {
|
|
|
541
567
|
: []),
|
|
542
568
|
...(missing.length > 0 ? [`${missing.join(", ")} could not be read from disk`] : []),
|
|
543
569
|
];
|
|
544
|
-
const content = notes.length > 0 ? `${said}
|
|
545
|
-
|
|
570
|
+
const content = notes.length > 0 ? `${said}
|
|
571
|
+
|
|
546
572
|
[${notes.join("; ")}]` : said;
|
|
547
573
|
messages.push({ role: "user", content, ...(images.length > 0 ? { images } : {}) });
|
|
548
574
|
continue;
|
|
@@ -550,10 +576,15 @@ directoryNotes = []) {
|
|
|
550
576
|
messages.push({ role: "user", content: said });
|
|
551
577
|
}
|
|
552
578
|
else if (e.role === "assistant") {
|
|
579
|
+
const calls = e.toolCalls && e.toolCalls.length > 0;
|
|
553
580
|
messages.push({
|
|
554
581
|
role: "assistant",
|
|
555
|
-
|
|
556
|
-
|
|
582
|
+
// A reply with no words and no tool calls (an empty completion, a filtered one) is
|
|
583
|
+
// stored as it came, but several providers reject an empty assistant message and it
|
|
584
|
+
// would be sent on every later request. A placeholder goes on the wire instead; a
|
|
585
|
+
// message that carries tool calls may have no words and is left alone.
|
|
586
|
+
content: calls || e.content.trim() !== "" ? e.content : EMPTY_REPLY_PLACEHOLDER,
|
|
587
|
+
...(calls ? { tool_calls: toWire(e.toolCalls) } : {}),
|
|
557
588
|
});
|
|
558
589
|
}
|
|
559
590
|
else {
|
|
@@ -567,7 +598,7 @@ directoryNotes = []) {
|
|
|
567
598
|
// END of the system prompt. The end, because everything before it is the prefix every
|
|
568
599
|
// session shares and caches against; appending leaves that untouched.
|
|
569
600
|
const agentPrompt = session.toolContext.agentPrompt;
|
|
570
|
-
const base = staticSystemPrompt(session.projectContext, session.projectMemory, session.memoryDir, session.memoryIndex, gov, workspaceText(session), session.priorSessions);
|
|
601
|
+
const base = staticSystemPrompt(session.projectContext, session.projectMemory, session.memoryDir, session.memoryIndex, gov, workspaceText(session), session.priorSessions, profilePrompt(readProfile()));
|
|
571
602
|
return {
|
|
572
603
|
system: agentPrompt ? `${base}\n\n${agentPrompt}` : base,
|
|
573
604
|
messages,
|
|
@@ -582,6 +613,11 @@ directoryNotes = []) {
|
|
|
582
613
|
// the command's own result says where it is now.
|
|
583
614
|
session.toolContext.cwdResetFrom && session.toolContext.cwd === session.cwd
|
|
584
615
|
? relativize(session.toolContext, session.toolContext.cwdResetFrom)
|
|
616
|
+
: "",
|
|
617
|
+
// Lead agent only: a fork inherits `marathon` by reference, and a verifier told to
|
|
618
|
+
// "keep working toward the goal" would stop being an independent check.
|
|
619
|
+
session.marathon?.status === "running" && (session.toolContext.subagentDepth ?? 0) === 0
|
|
620
|
+
? marathonBlock(session.marathon.goal, session.marathon.turnsSpent === 0 ? "plan" : "run")
|
|
585
621
|
: ""),
|
|
586
622
|
tools,
|
|
587
623
|
model: session.modelConfig,
|
|
@@ -607,14 +643,27 @@ async function backgroundEventNotes(session) {
|
|
|
607
643
|
* app", which blamed the user for the agent's own restart and gave the model a second,
|
|
608
644
|
* contradictory account of the same event.
|
|
609
645
|
*/
|
|
610
|
-
export function backgroundEventNote({ info, kind, tail, wake, }) {
|
|
646
|
+
export function backgroundEventNote({ info, kind, tail, wake, ports, }) {
|
|
611
647
|
// It came up. This is the only positive event a server ever produces, and it is
|
|
612
648
|
// what lets the model actually deliver the "I'll tell you when it's running" it
|
|
613
649
|
// was told to say. Nothing has gone wrong, so there is nothing to fix.
|
|
650
|
+
// Something it started opened a port after it was already running: typically the app
|
|
651
|
+
// a build was producing has opened (its window, its debugging port), which is what an
|
|
652
|
+
// agent that ended its turn to "wait for the app" was waiting for.
|
|
653
|
+
if (kind === "opened") {
|
|
654
|
+
const list = (ports ?? []).join(", ");
|
|
655
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) opened port ${list}.]\n` +
|
|
656
|
+
`Recent output:\n${tail || "(no output)"}\n\n` +
|
|
657
|
+
`Something it started is now listening there: usually the app or server you were waiting for has ` +
|
|
658
|
+
`opened. If you were waiting for it, carry on now (for an app's debugging port, call ui with that ` +
|
|
659
|
+
`port). If you were not, there is nothing to do. Nothing is wrong: do not restart it.`);
|
|
660
|
+
}
|
|
614
661
|
if (kind === "ready") {
|
|
615
|
-
|
|
662
|
+
const on = info.listening?.length ? ` (listening on port ${info.listening.join(", ")})` : "";
|
|
663
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) is up and running${on}.]\n` +
|
|
616
664
|
`Recent output:\n${tail || "(no output)"}\n\n` +
|
|
617
|
-
`Tell the user in one short line that it's running
|
|
665
|
+
`Tell the user in one short line that it's running, and do not repeat anything you already said ` +
|
|
666
|
+
`about it. Nothing is wrong — do not investigate, ` +
|
|
618
667
|
`do not restart it, and do not change any files because of this. Running only means the ` +
|
|
619
668
|
`process started: do not describe what it shows or say a change is visible unless you ` +
|
|
620
669
|
`have actually looked.`);
|
|
@@ -702,7 +751,7 @@ export function callIsConcurrencySafe(tool, args) {
|
|
|
702
751
|
* an unreadable rules directory must not take the turn down with it — on any failure the
|
|
703
752
|
* session simply keeps the governance it already had.
|
|
704
753
|
*/
|
|
705
|
-
async function refreshGovernance(session, force = false) {
|
|
754
|
+
export async function refreshGovernance(session, force = false) {
|
|
706
755
|
try {
|
|
707
756
|
const stamp = await governanceStamp(session.toolContext.cwd);
|
|
708
757
|
// Skipping when the stamp is unchanged is what keeps our OWN writes from causing a
|
|
@@ -763,13 +812,13 @@ export async function respond(session, options = {}) {
|
|
|
763
812
|
function implementFromScratch(priorPath, plan) {
|
|
764
813
|
const path = priorPath;
|
|
765
814
|
const where = path
|
|
766
|
-
? `
|
|
767
|
-
|
|
815
|
+
? `
|
|
816
|
+
|
|
768
817
|
If you need something exact from the planning that produced this — a snippet, an ` +
|
|
769
818
|
`error message, a path — the full conversation is at: ${path}`
|
|
770
819
|
: "";
|
|
771
|
-
return `Implement the following plan:
|
|
772
|
-
|
|
820
|
+
return `Implement the following plan:
|
|
821
|
+
|
|
773
822
|
${plan}${where}`;
|
|
774
823
|
}
|
|
775
824
|
/**
|
|
@@ -806,11 +855,23 @@ async function respondTurn(session, options = {}) {
|
|
|
806
855
|
// every subsequent turn, and it keeps `activeDriver()` safe to call synchronously
|
|
807
856
|
// from here down (including from inside a tool).
|
|
808
857
|
await ensureDriver(session.modelConfig.model);
|
|
858
|
+
// Bring the user's usage limits up to date, once per top-level turn. Cheap when they are off
|
|
859
|
+
// (one small file read); a sub-agent's turn is inside this one and does not repeat it.
|
|
860
|
+
if ((session.toolContext.subagentDepth ?? 0) === 0)
|
|
861
|
+
await refreshUsageLimits().catch(() => { });
|
|
809
862
|
// Pick up a governance file the USER edited by hand since the last turn. One stat
|
|
810
863
|
// pass over a few small directories, taken here because a turn is the only moment
|
|
811
864
|
// governance is consulted — so it is fresh exactly where it is used, with no watcher
|
|
812
865
|
// to own, poll or tear down. See refreshGovernance.
|
|
813
866
|
await refreshGovernance(session);
|
|
867
|
+
// A history the provider would reject is healed here, before anything is sent. The usual
|
|
868
|
+
// cause is a turn that ended with tool calls still unanswered (a tool that threw, a crash
|
|
869
|
+
// between the call and its result): every later request would then be refused, and the only
|
|
870
|
+
// cure used to be restarting, which repairs the same thing on load. Both repairs return the
|
|
871
|
+
// same array when there is nothing to do, so a healthy session pays one scan per turn.
|
|
872
|
+
const healed = reconcileInterruptedTools(repairToolCallOrder(session.transcript), "the turn ended before this tool returned");
|
|
873
|
+
if (healed !== session.transcript)
|
|
874
|
+
session.transcript = healed;
|
|
814
875
|
// Resume an approved plan from disk, once per session (undefined = unchecked).
|
|
815
876
|
// A plan approved last session is still the agreed scope this session — that is
|
|
816
877
|
// the point of it being an artifact — and the user deleting the file (or its
|
|
@@ -865,7 +926,13 @@ async function respondTurn(session, options = {}) {
|
|
|
865
926
|
// Session): fork a scoped child, forward the child's usage to this turn's meter,
|
|
866
927
|
// and share this turn's abort signal so Esc stops a sub-agent too.
|
|
867
928
|
session.toolContext.forkChild = (task, opts) => forkSession(session, task, opts);
|
|
868
|
-
session.toolContext.reportUsage = (u) =>
|
|
929
|
+
session.toolContext.reportUsage = (u) => {
|
|
930
|
+
options.onEvent?.({ type: "usage", ...u });
|
|
931
|
+
// A sub-agent's calls arrive here as events, but its own turn already counted them toward the
|
|
932
|
+
// limits; only a bare usage (a page-summarising call, say) is new.
|
|
933
|
+
if (!("type" in u))
|
|
934
|
+
countUsage(session, u, options);
|
|
935
|
+
};
|
|
869
936
|
// The raw event sink, so spawn_subagent can surface a child's nested activity
|
|
870
937
|
// (its lifecycle + tagged tool calls) up this same stream instead of running dark.
|
|
871
938
|
session.toolContext.emitEvent = options.onEvent;
|
|
@@ -941,6 +1008,7 @@ async function respondTurn(session, options = {}) {
|
|
|
941
1008
|
// Narration budget: one nudge per turn, and the turn's earlier prose to compare against.
|
|
942
1009
|
let narrationNudged = false;
|
|
943
1010
|
const narratedBefore = [];
|
|
1011
|
+
const readThisTurn = new Set(); // files read so far, for narrationShown
|
|
944
1012
|
// Judged next to the prose, pushed after the tool results — see the gate below.
|
|
945
1013
|
let pendingNarrationFault = null;
|
|
946
1014
|
let lastFailSig = null;
|
|
@@ -957,6 +1025,12 @@ async function respondTurn(session, options = {}) {
|
|
|
957
1025
|
// no matter how the turn ends (finish, pause, interrupt, throw). Labeled with
|
|
958
1026
|
// the request that drove it. No-op when nothing was edited.
|
|
959
1027
|
const turnLabel = lastUserText(session);
|
|
1028
|
+
// The message that opened this turn, stamped now if the save that normally stamps it
|
|
1029
|
+
// has not run yet. The checkpoint below carries that stamp, which is how a rewind to
|
|
1030
|
+
// this message (or any earlier one) finds the file changes to take back with it.
|
|
1031
|
+
const opener = turnOpener(session);
|
|
1032
|
+
if (opener && opener.ts === undefined)
|
|
1033
|
+
opener.ts = Date.now();
|
|
960
1034
|
// Fold this turn's cost into the session total on the way out, however the turn ends.
|
|
961
1035
|
// In the `finally` rather than the success path on purpose: an interrupted or failed
|
|
962
1036
|
// turn still spent the tokens it spent, and a spend figure that quietly omits the
|
|
@@ -993,7 +1067,7 @@ async function respondTurn(session, options = {}) {
|
|
|
993
1067
|
finally {
|
|
994
1068
|
recordSpend();
|
|
995
1069
|
const before = session.toolContext.checkpoints?.list().length ?? 0;
|
|
996
|
-
session.toolContext.checkpoints?.seal(turnLabel);
|
|
1070
|
+
session.toolContext.checkpoints?.seal(turnLabel, opener?.ts);
|
|
997
1071
|
// Say that a restore point exists. It was made silently, so `/undo` was a feature
|
|
998
1072
|
// you had to already know about — and the moment to learn it is the moment there is
|
|
999
1073
|
// something to undo, not after you have lost it.
|
|
@@ -1007,11 +1081,16 @@ async function respondTurn(session, options = {}) {
|
|
|
1007
1081
|
for (let step = 0; stepLimit === undefined || step < stepLimit; step++) {
|
|
1008
1082
|
if (options.signal?.aborted)
|
|
1009
1083
|
return interrupted(session);
|
|
1084
|
+
// The user's own usage limit: a step that began under it has finished, so this is the
|
|
1085
|
+
// boundary. Nothing is lost; the work carries on once the window reopens.
|
|
1086
|
+
const heldBack = (session.toolContext.subagentDepth ?? 0) === 0 ? limitGateReason() : null;
|
|
1087
|
+
if (heldBack)
|
|
1088
|
+
return pauseTask(session, options, heldBack, "limit");
|
|
1010
1089
|
// Stop before another (billable) call if a cost/time ceiling is hit — pause
|
|
1011
1090
|
// losslessly, exactly like the step budget, so the user can raise it and resume.
|
|
1012
1091
|
const limitReason = taskLimitReason(summarizeTask(usages, session.modelConfig.model), Date.now() - startedAt, limits);
|
|
1013
1092
|
if (limitReason)
|
|
1014
|
-
return pauseTask(session, options, `hit the ${limitReason}
|
|
1093
|
+
return pauseTask(session, options, `hit the ${limitReason}`, "costTimeLimit");
|
|
1015
1094
|
await maybeCompact(session, options);
|
|
1016
1095
|
// NO working-set block is built or sent. It used to be: the current contents of
|
|
1017
1096
|
// every active file, rebuilt each step and injected at the tail — up to 12K tokens
|
|
@@ -1111,7 +1190,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1111
1190
|
if (!shed)
|
|
1112
1191
|
return false;
|
|
1113
1192
|
overflowRecovered = true;
|
|
1114
|
-
session
|
|
1193
|
+
replaceTranscript(session, shed);
|
|
1115
1194
|
await options.persist?.();
|
|
1116
1195
|
options.onActivity?.("conversation was too long — dropped the oldest turns and retried", {
|
|
1117
1196
|
context: true,
|
|
@@ -1144,6 +1223,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1144
1223
|
// may span several calls across tool rounds, and the UI sums them.
|
|
1145
1224
|
emitUsage(result, options);
|
|
1146
1225
|
if (result.usage) {
|
|
1226
|
+
countUsage(session, result.usage, options);
|
|
1147
1227
|
usages.push(result.usage);
|
|
1148
1228
|
usageTimes.push(Date.now());
|
|
1149
1229
|
writeCacheLog(cacheCallLine({
|
|
@@ -1180,7 +1260,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1180
1260
|
if (content.trim())
|
|
1181
1261
|
session.transcript.push({ role: "assistant", content });
|
|
1182
1262
|
await options.persist?.();
|
|
1183
|
-
return pauseTask(session, options, note);
|
|
1263
|
+
return pauseTask(session, options, note, result.stop);
|
|
1184
1264
|
}
|
|
1185
1265
|
// No tool calls → the model is done. Record the reply.
|
|
1186
1266
|
if (toolCalls.length === 0) {
|
|
@@ -1246,6 +1326,13 @@ async function respondTurn(session, options = {}) {
|
|
|
1246
1326
|
// Durable BEFORE running the tools: if the machine dies mid-tool, the resume path
|
|
1247
1327
|
// sees these dangling tool_calls and reconciles them (reconcileInterruptedTools).
|
|
1248
1328
|
await options.persist?.();
|
|
1329
|
+
// Whether the words above lead to anything on screen, before any tool is announced.
|
|
1330
|
+
{
|
|
1331
|
+
const calls = toolCalls.map((c) => ({ name: c.name, args: parseArgs(c.arguments) }));
|
|
1332
|
+
if (content.trim())
|
|
1333
|
+
options.onEvent?.({ type: "narration", shown: narrationShown(calls, readThisTurn) });
|
|
1334
|
+
noteReads(calls, readThisTurn);
|
|
1335
|
+
}
|
|
1249
1336
|
// Narration gate, part one: JUDGE here, where this message's prose and the turn's
|
|
1250
1337
|
// earlier prose are both in hand. Do NOT push anything yet — an assistant message
|
|
1251
1338
|
// carrying tool_calls must be followed immediately by a tool message per call, and
|
|
@@ -1259,11 +1346,20 @@ async function respondTurn(session, options = {}) {
|
|
|
1259
1346
|
// Announce every tool the model chose, in its order, BEFORE running any —
|
|
1260
1347
|
// the UI's reveal queue paces them and a slow tool (test/run) can show a live
|
|
1261
1348
|
// "running" state until its end event lands.
|
|
1262
|
-
|
|
1349
|
+
// A row is announced when its tool actually STARTS, not up front for the whole step.
|
|
1350
|
+
// Announcing every call at once showed a command as running while a Sentinel prompt
|
|
1351
|
+
// for an earlier call was still waiting, and kept rows appearing behind an open
|
|
1352
|
+
// question: work on screen that nobody had approved. A call that never runs (declined,
|
|
1353
|
+
// refused, interrupted) is announced together with its end, so it still gets its row.
|
|
1354
|
+
const announced = new Set();
|
|
1355
|
+
const announce = (call) => {
|
|
1356
|
+
if (announced.has(call.id))
|
|
1357
|
+
return;
|
|
1358
|
+
announced.add(call.id);
|
|
1263
1359
|
options.onEvent?.({ type: "tool", phase: "start", id: call.id, name: call.name, args: parseArgs(call.arguments) });
|
|
1264
|
-
}
|
|
1265
|
-
//
|
|
1266
|
-
// (parallel edits to one file race, and an edit must see the last write). A call
|
|
1360
|
+
};
|
|
1361
|
+
// Consecutive concurrency-safe calls run in PARALLEL; every other call runs alone, in
|
|
1362
|
+
// order (parallel edits to one file race, and an edit must see the last write). A call
|
|
1267
1363
|
// is concurrency-safe when the tool says so for THESE args (isConcurrencySafe) —
|
|
1268
1364
|
// e.g. a read-only sub-agent, which lets the model fan out research — otherwise
|
|
1269
1365
|
// the default is: read-only ⇒ safe, mutating ⇒ serial.
|
|
@@ -1271,8 +1367,9 @@ async function respondTurn(session, options = {}) {
|
|
|
1271
1367
|
const tool = lookup(call.name);
|
|
1272
1368
|
return tool ? callIsConcurrencySafe(tool, parseArgs(call.arguments)) : false;
|
|
1273
1369
|
};
|
|
1274
|
-
|
|
1275
|
-
|
|
1370
|
+
// Batches keep the model's ORDER (toolBatches.ts): a read written after an edit has to
|
|
1371
|
+
// see the edit, so read-only calls are only grouped with their neighbours.
|
|
1372
|
+
const batches = partitionCalls(toolCalls, concurrencySafe);
|
|
1276
1373
|
const runCall = async (call) => {
|
|
1277
1374
|
// Esc: once the turn is aborted, no further tool may START. The step loop only
|
|
1278
1375
|
// re-checks BETWEEN steps, so without this gate the rest of a batch still runs
|
|
@@ -1345,7 +1442,10 @@ async function respondTurn(session, options = {}) {
|
|
|
1345
1442
|
// uniformly — including subagent edits. Fails safe: no approval channel, or an
|
|
1346
1443
|
// unclear answer, refuses rather than runs.
|
|
1347
1444
|
const ctx = session.toolContext;
|
|
1348
|
-
|
|
1445
|
+
// Two kinds of standing "yes": one given during this session (guardAllowed), and
|
|
1446
|
+
// one the user saved in their permissions, for this project or every project.
|
|
1447
|
+
const savedYes = ctx.governance?.sentinelAllow?.includes(call.name) === true;
|
|
1448
|
+
if (!tool.readOnly && ctx.guarded && !ctx.guardAllowed?.has(call.name) && !savedYes) {
|
|
1349
1449
|
const args = parseArgs(call.arguments);
|
|
1350
1450
|
// The question is one line; WHAT is about to happen rides as detail, which the
|
|
1351
1451
|
// CLI prints into the transcript. A gate the user cannot read is a gate they
|
|
@@ -1390,21 +1490,61 @@ async function respondTurn(session, options = {}) {
|
|
|
1390
1490
|
// Zero threw — every one returned an error result. So this catches nothing today
|
|
1391
1491
|
// and is deliberately defence in depth: it removes a single point of failure
|
|
1392
1492
|
// rather than fixing an observed bug, and the next tool added inherits it.
|
|
1493
|
+
announce(call); // approved (or needing no approval): now it is really running
|
|
1494
|
+
// A command waits until its row is actually on screen before it starts, when the caller
|
|
1495
|
+
// paces the screen. Otherwise the screen, which shows one thing at a time, replays a
|
|
1496
|
+
// command that finished long ago and its "running" state is never real. Only commands:
|
|
1497
|
+
// reads and edits take milliseconds, and holding them back would only slow the turn.
|
|
1498
|
+
if (tool.liveRow && options.beforeLiveTool) {
|
|
1499
|
+
await options.beforeLiveTool(call.id);
|
|
1500
|
+
if (options.signal?.aborted) {
|
|
1501
|
+
return {
|
|
1502
|
+
call,
|
|
1503
|
+
output: "Not run: the turn was interrupted before this tool started.",
|
|
1504
|
+
summary: "interrupted",
|
|
1505
|
+
isError: true,
|
|
1506
|
+
detail: undefined,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1393
1510
|
let result;
|
|
1394
1511
|
try {
|
|
1395
1512
|
// A per-call channel, so a tool that runs for minutes can say what it is doing.
|
|
1396
1513
|
// Scoped to THIS call rather than hung on the shared context, which every tool in
|
|
1397
1514
|
// the turn holds the same instance of — with two commands in flight there would be
|
|
1398
1515
|
// no way to tell whose output was whose.
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1516
|
+
// Memories, rules, skills, permissions and MCP servers are written by their own
|
|
1517
|
+
// helpers, not the edit tools; snapshotting around the call puts them in the same
|
|
1518
|
+
// undo net as project files, so /undo and a rewind take them back too.
|
|
1519
|
+
const stateBefore = session.toolContext.checkpoints && writesAgentState(call.name)
|
|
1520
|
+
? await snapshotAgentState(session.toolContext)
|
|
1521
|
+
: null;
|
|
1522
|
+
try {
|
|
1523
|
+
result = await tool.execute(parseArgs(call.arguments), session.toolContext, {
|
|
1524
|
+
progress: (text) => options.onEvent?.({ type: "tool", phase: "progress", id: call.id, text }),
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
finally {
|
|
1528
|
+
if (stateBefore)
|
|
1529
|
+
await recordAgentStateChanges(session.toolContext, stateBefore);
|
|
1530
|
+
}
|
|
1402
1531
|
}
|
|
1403
1532
|
catch (error) {
|
|
1404
|
-
// An abort is the user, not a fault
|
|
1405
|
-
//
|
|
1406
|
-
|
|
1407
|
-
|
|
1533
|
+
// An abort is the user, not a fault, so it is NOT reported as a broken tool. But it
|
|
1534
|
+
// must not travel out of the loop either: the assistant message carrying these calls
|
|
1535
|
+
// is already saved, so a throw here leaves calls with no results, and every later
|
|
1536
|
+
// request in the live session is then malformed until the app is restarted. It is
|
|
1537
|
+
// recorded as an interrupted call instead; when the turn really was stopped the next
|
|
1538
|
+
// pass through the loop sees the abort and ends the turn cleanly.
|
|
1539
|
+
if (isAbort(error)) {
|
|
1540
|
+
return {
|
|
1541
|
+
call,
|
|
1542
|
+
output: "Not completed: this call was interrupted before it finished, so its effect is unknown. Check the current state before relying on it.",
|
|
1543
|
+
summary: "interrupted",
|
|
1544
|
+
isError: true,
|
|
1545
|
+
detail: undefined,
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1408
1548
|
return { call, ...toolFailureResult(call.name, error), detail: undefined };
|
|
1409
1549
|
}
|
|
1410
1550
|
return {
|
|
@@ -1418,6 +1558,8 @@ async function respondTurn(session, options = {}) {
|
|
|
1418
1558
|
quiet: result.quiet,
|
|
1419
1559
|
fullContentOf: result.fullContentOf,
|
|
1420
1560
|
images: result.images,
|
|
1561
|
+
web: result.web,
|
|
1562
|
+
ui: result.ui,
|
|
1421
1563
|
displayKind: result.displayKind,
|
|
1422
1564
|
displayName: result.displayName,
|
|
1423
1565
|
awaitsModel: result.awaitsModel,
|
|
@@ -1429,6 +1571,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1429
1571
|
// the model's call order (the sort below); only the UI events go out eagerly.
|
|
1430
1572
|
const runAndEmit = async (call) => {
|
|
1431
1573
|
const r = await runCall(call);
|
|
1574
|
+
announce(r.call); // one that never ran gets its start now, right before its end
|
|
1432
1575
|
options.onEvent?.({
|
|
1433
1576
|
type: "tool",
|
|
1434
1577
|
phase: "end",
|
|
@@ -1442,12 +1585,18 @@ async function respondTurn(session, options = {}) {
|
|
|
1442
1585
|
...(r.quiet ? { quiet: true } : {}),
|
|
1443
1586
|
...(r.displayKind ? { displayKind: r.displayKind } : {}),
|
|
1444
1587
|
...(r.displayName ? { displayName: r.displayName } : {}),
|
|
1588
|
+
...(r.images?.length ? { images: r.images.map((i) => i.path) } : {}),
|
|
1589
|
+
...(r.web ? { web: r.web } : {}),
|
|
1590
|
+
...(r.ui ? { ui: r.ui } : {}),
|
|
1445
1591
|
});
|
|
1446
1592
|
return r;
|
|
1447
1593
|
};
|
|
1448
|
-
const results =
|
|
1449
|
-
for (const
|
|
1450
|
-
|
|
1594
|
+
const results = [];
|
|
1595
|
+
for (const batch of batches) {
|
|
1596
|
+
if (batch.parallel)
|
|
1597
|
+
results.push(...(await runLimited(batch.calls, MAX_PARALLEL_CALLS, runAndEmit)));
|
|
1598
|
+
else
|
|
1599
|
+
results.push(await runAndEmit(batch.calls[0]));
|
|
1451
1600
|
}
|
|
1452
1601
|
// Hand results back in the model's original call order (start events were emitted
|
|
1453
1602
|
// in that order too), no matter which lane each call ran in.
|
|
@@ -1492,6 +1641,9 @@ async function respondTurn(session, options = {}) {
|
|
|
1492
1641
|
...(result.quiet ? { quiet: true } : {}),
|
|
1493
1642
|
...(result.displayName ? { displayName: result.displayName } : {}),
|
|
1494
1643
|
...(result.displayKind ? { displayKind: result.displayKind } : {}),
|
|
1644
|
+
...(result.images?.length ? { imagePaths: result.images.map((i) => i.path) } : {}),
|
|
1645
|
+
...(result.web ? { web: result.web } : {}),
|
|
1646
|
+
...(result.ui ? { ui: result.ui } : {}),
|
|
1495
1647
|
...(result.isError ? { isError: true } : {}),
|
|
1496
1648
|
// Presence, as recorded by the tool that knows: this result IS the whole
|
|
1497
1649
|
// content of that file. Not display — the presence derivation reads it.
|
|
@@ -1761,10 +1913,19 @@ async function respondTurn(session, options = {}) {
|
|
|
1761
1913
|
// transcript, task list, and working set are all intact, so telling Mindweave to
|
|
1762
1914
|
// continue resumes exactly here with nothing lost, and the user stays in control of
|
|
1763
1915
|
// the spend.
|
|
1764
|
-
return pauseTask(session, options, `reached the step budget of ${stepLimit} tool steps in one turn
|
|
1916
|
+
return pauseTask(session, options, `reached the step budget of ${stepLimit} tool steps in one turn`, "stepBudget");
|
|
1765
1917
|
}
|
|
1766
1918
|
}
|
|
1767
1919
|
/** The most recent user request in the transcript, clipped — labels a checkpoint. */
|
|
1920
|
+
/** The last message the person sent, which is what this turn is answering. */
|
|
1921
|
+
function turnOpener(session) {
|
|
1922
|
+
for (let i = session.transcript.length - 1; i >= 0; i--) {
|
|
1923
|
+
const e = session.transcript[i];
|
|
1924
|
+
if (e.role === "user" && !e.synthetic)
|
|
1925
|
+
return e;
|
|
1926
|
+
}
|
|
1927
|
+
return undefined;
|
|
1928
|
+
}
|
|
1768
1929
|
function lastUserText(session) {
|
|
1769
1930
|
for (let i = session.transcript.length - 1; i >= 0; i--) {
|
|
1770
1931
|
const e = session.transcript[i];
|
|
@@ -1796,6 +1957,7 @@ function endTurnWith(session, options, msg) {
|
|
|
1796
1957
|
* one in the same turn (the re-scope guard) — a natural checkpoint to let the user
|
|
1797
1958
|
* steer instead of the model taking on scope it wasn't asked for. */
|
|
1798
1959
|
function pauseReScope(session, options) {
|
|
1960
|
+
options.onPause?.("reScope");
|
|
1799
1961
|
return endTurnWith(session, options, "(I've finished the task list for what you asked. I have ideas for taking it " +
|
|
1800
1962
|
"further, but I've stopped here so you can steer — rather than piling on new scope " +
|
|
1801
1963
|
'on my own. Tell me which direction you want, or say "keep going" to continue.)');
|
|
@@ -1805,6 +1967,7 @@ function pauseReScope(session, options) {
|
|
|
1805
1967
|
* to do but wait — end the turn cleanly instead of looping "still running" checks.
|
|
1806
1968
|
* Deliberately worded as a status line to the user, not a "paused" apology. */
|
|
1807
1969
|
function pauseForBackgroundPoll(session, options) {
|
|
1970
|
+
options.onPause?.("backgroundPoll");
|
|
1808
1971
|
return endTurnWith(session, options, "It's still running in the background. I'll stop checking and let you know as soon " +
|
|
1809
1972
|
"as it finishes — no need to keep watching.");
|
|
1810
1973
|
}
|
|
@@ -1813,13 +1976,22 @@ function pauseForBackgroundPoll(session, options) {
|
|
|
1813
1976
|
* count, and its real working directory, and it still hasn't moved — so hand the wheel
|
|
1814
1977
|
* to the user rather than spend more steps on it. */
|
|
1815
1978
|
function pauseForRepeatedFailure(session, options, errorOutput) {
|
|
1979
|
+
options.onPause?.("repeatedFailure");
|
|
1816
1980
|
return endTurnWith(session, options, `I've hit the same failure several times in a row and I'm not making progress, so I've ` +
|
|
1817
1981
|
`stopped rather than retry the same thing again. The error was:\n\n${firstErrorLine(errorOutput)}\n\n` +
|
|
1818
1982
|
`Tell me how you'd like to proceed, or I can try a different approach.`);
|
|
1819
1983
|
}
|
|
1820
1984
|
/** Record and return a clean, lossless pause reply (well-formed transcript) when a
|
|
1821
|
-
* guard trips — step budget or a cost/time ceiling. Saying "continue" resumes.
|
|
1822
|
-
|
|
1985
|
+
* guard trips — step budget or a cost/time ceiling. Saying "continue" resumes.
|
|
1986
|
+
* `tag` tells the two apart for `onPause`; the prose reason stays a free string since
|
|
1987
|
+
* it already carries the exact number ("the step budget of 40 tool steps"). */
|
|
1988
|
+
function pauseTask(session, options, reason, tag) {
|
|
1989
|
+
options.onPause?.(tag);
|
|
1990
|
+
// A usage limit is not something to argue with by saying "continue": it lifts on its own.
|
|
1991
|
+
if (tag === "limit") {
|
|
1992
|
+
return endTurnWith(session, options, `(Paused — ${reason}. The task isn't finished, but nothing is lost: your progress, edits, and task ` +
|
|
1993
|
+
`list are saved. Carry on once it opens.)`);
|
|
1994
|
+
}
|
|
1823
1995
|
return endTurnWith(session, options, `(Paused — ${reason}. The task isn't finished, but nothing is lost: your progress, ` +
|
|
1824
1996
|
`edits, and task list are saved. Say "continue" to pick up exactly where I left off.)`);
|
|
1825
1997
|
}
|
|
@@ -1874,6 +2046,18 @@ export function toCallRecord(u, model, at = Date.now()) {
|
|
|
1874
2046
|
model,
|
|
1875
2047
|
};
|
|
1876
2048
|
}
|
|
2049
|
+
/**
|
|
2050
|
+
* A finished model call counts toward the user's limits, and a window crossing 80% or 95%
|
|
2051
|
+
* says so once, as a line in the conversation. Only the top-level turn speaks: a sub-agent's
|
|
2052
|
+
* call is counted, and the parent's next call is what reports where things stand.
|
|
2053
|
+
*/
|
|
2054
|
+
function countUsage(session, u, options) {
|
|
2055
|
+
noteUsage(session.modelConfig.model, u);
|
|
2056
|
+
if ((session.toolContext.subagentDepth ?? 0) > 0)
|
|
2057
|
+
return;
|
|
2058
|
+
for (const line of takeLimitWarnings())
|
|
2059
|
+
options.onActivity?.(`Usage limit: ${line}`);
|
|
2060
|
+
}
|
|
1877
2061
|
/** Report a turn's token usage to the UI, if the provider returned it. */
|
|
1878
2062
|
function emitUsage(result, options) {
|
|
1879
2063
|
if (result.usage) {
|
|
@@ -1908,7 +2092,7 @@ async function sweepSessionMemory(session, options) {
|
|
|
1908
2092
|
const grown = shouldUpdateSessionMemory(estimateEntriesTokens(session.transcript), session.sessionMemoryTokens ?? 0, session.sessionMemoryInit ?? false);
|
|
1909
2093
|
if (!grown)
|
|
1910
2094
|
return;
|
|
1911
|
-
await updateSessionMemory(session);
|
|
2095
|
+
await updateSessionMemory(session, options.signal);
|
|
1912
2096
|
await options.persist?.(); // durable: the notes sidecar is written by the persister
|
|
1913
2097
|
}
|
|
1914
2098
|
/**
|
|
@@ -1938,12 +2122,39 @@ export function contextUsed(session) {
|
|
|
1938
2122
|
: (session.toolContext.mcp?.estimatedTokens() ?? 0);
|
|
1939
2123
|
return estimateEntriesTokens(session.transcript) + overhead;
|
|
1940
2124
|
}
|
|
2125
|
+
/**
|
|
2126
|
+
* The auto-compaction bar actually in force for this session: the user's own override
|
|
2127
|
+
* (Settings > Usage > Context, or `/context limit`) if they set one — project first,
|
|
2128
|
+
* then all-projects, per governor/index.ts's context.json — else Mindweave's own
|
|
2129
|
+
* model-anchored default. The env var still wins over either, same as before this
|
|
2130
|
+
* existed: it is the sandbox/CI escape hatch, not a user-facing setting.
|
|
2131
|
+
*/
|
|
2132
|
+
export function effectiveAutoCompactThreshold(session) {
|
|
2133
|
+
// Optional chaining: a real session always has governance (loadGovernance runs at
|
|
2134
|
+
// session start), but a hand-built test fixture may skip it, and a pure accessor
|
|
2135
|
+
// shouldn't crash on that rather than just falling through to the model default.
|
|
2136
|
+
const base = session.governance?.contextAutoCompactTokens ?? autoCompactThreshold(session.modelConfig.model);
|
|
2137
|
+
return envInt("MINDWEAVE_AUTOCOMPACT_TOKENS", base);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* How full the context is, against the bar auto-compaction fires at: what a front end
|
|
2141
|
+
* draws as the context meter. The same numbers `maybeCompact` decides on, so a full meter
|
|
2142
|
+
* and a compaction cannot disagree.
|
|
2143
|
+
*/
|
|
2144
|
+
export function contextFill(session) {
|
|
2145
|
+
return {
|
|
2146
|
+
used: contextUsed(session),
|
|
2147
|
+
limit: effectiveAutoCompactThreshold(session),
|
|
2148
|
+
window: sharpContextWindow(session.modelConfig.model),
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
1941
2151
|
async function maybeCompact(session, options) {
|
|
1942
2152
|
const model = session.modelConfig.model;
|
|
1943
|
-
// Model-anchored bars (env still
|
|
1944
|
-
// instead of a fixed number — and a longer/stronger
|
|
2153
|
+
// Model-anchored bars (env, then the user's own override, still win), so the
|
|
2154
|
+
// thresholds are right per model instead of a fixed number — and a longer/stronger
|
|
2155
|
+
// model automatically gets more room.
|
|
1945
2156
|
const microBar = envInt("MINDWEAVE_MICROCOMPACT_TOKENS", microCompactThreshold(model));
|
|
1946
|
-
const autoBar =
|
|
2157
|
+
const autoBar = effectiveAutoCompactThreshold(session);
|
|
1947
2158
|
// MCP tool schemas are sent on every turn but live OUTSIDE the transcript, so the bars
|
|
1948
2159
|
// could not see them: a 30K-token catalog meant the model was 30K deeper into its real
|
|
1949
2160
|
// context than this arithmetic believed, and every threshold fired that much too late.
|
|
@@ -2048,6 +2259,16 @@ export async function compactNow(session, options = {}) {
|
|
|
2048
2259
|
async function autocompact(session, options) {
|
|
2049
2260
|
if (session.transcript.length === 0)
|
|
2050
2261
|
return;
|
|
2262
|
+
options.onCompactionStart?.();
|
|
2263
|
+
try {
|
|
2264
|
+
await summarizeAndSplice(session, options);
|
|
2265
|
+
}
|
|
2266
|
+
finally {
|
|
2267
|
+
options.onCompactionEnd?.();
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
/** The body of `autocompact`: the notes when they cover the prefix, else the summarizer. */
|
|
2271
|
+
async function summarizeAndSplice(session, options) {
|
|
2051
2272
|
// Measured BEFORE the summarizer runs, with the same arithmetic the thresholds use,
|
|
2052
2273
|
// so the bar the user sees is the number the system actually acted on.
|
|
2053
2274
|
const before = contextUsed(session);
|
|
@@ -2069,9 +2290,9 @@ async function autocompact(session, options) {
|
|
|
2069
2290
|
// summarizer is about to be paid to produce. When they are current enough to cover
|
|
2070
2291
|
// the prefix being dropped, spending a model call buys something already owned.
|
|
2071
2292
|
// Declines rather than approximates: stale or empty notes fall through.
|
|
2072
|
-
const fromNotes = compactFromSessionMemory(session.transcript, session.sessionMemory, session.sessionMemoryEntries,
|
|
2293
|
+
const fromNotes = compactFromSessionMemory(session.transcript, session.sessionMemory, session.sessionMemoryEntries, effectiveAutoCompactThreshold(session), contextUsed(session) - estimateEntriesTokens(session.transcript));
|
|
2073
2294
|
if (fromNotes) {
|
|
2074
|
-
session
|
|
2295
|
+
replaceTranscript(session, fromNotes.entries);
|
|
2075
2296
|
// The notes now describe everything before the tail they were spliced in front of.
|
|
2076
2297
|
session.sessionMemoryEntries = 1;
|
|
2077
2298
|
session.sessionMemoryTokens = estimateEntriesTokens(session.transcript);
|
|
@@ -2081,15 +2302,31 @@ async function autocompact(session, options) {
|
|
|
2081
2302
|
let summary;
|
|
2082
2303
|
try {
|
|
2083
2304
|
// Summaries don't need reasoning — use the chosen model with thinking off.
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2305
|
+
// With reasoning off where the model allows it; see auxModel.ts.
|
|
2306
|
+
//
|
|
2307
|
+
// `ensureDriver` here, not just at the top of the turn: a sub-agent (or another
|
|
2308
|
+
// background aux call) can run its OWN model in between and leaves `activeDriver()`
|
|
2309
|
+
// pointed at THAT provider, which is a plain global, not scoped to this session. A
|
|
2310
|
+
// compaction landing after one then handed this session's model string to the
|
|
2311
|
+
// wrong provider's API, which correctly refused it as a model it had never heard
|
|
2312
|
+
// of ("Unknown Model, please check the model code.") — same failure shape as the
|
|
2313
|
+
// model call sites in respondTurn, which is why only THIS one call was missing it.
|
|
2314
|
+
//
|
|
2315
|
+
// `withTools`: a real (read-only) tool set, attached only if the model already
|
|
2316
|
+
// refused a bare call — see auxModel.ts for why some free models require it.
|
|
2317
|
+
const turn = await withAuxModel(session.modelConfig, async (model, withTools) => {
|
|
2318
|
+
await ensureDriver(model.model);
|
|
2319
|
+
return activeDriver().toolTurn({
|
|
2320
|
+
system: SUMMARY_SYSTEM_PROMPT,
|
|
2321
|
+
messages: [
|
|
2322
|
+
{
|
|
2323
|
+
role: "user",
|
|
2324
|
+
content: `${formatTranscriptForSummary(session.transcript)}\n\n${summaryRequest(options.compactFocus)}`,
|
|
2325
|
+
},
|
|
2326
|
+
],
|
|
2327
|
+
model,
|
|
2328
|
+
...(withTools ? { tools: toolSchemas({ readOnlyOnly: true }) } : {}),
|
|
2329
|
+
}, { signal: options.signal });
|
|
2093
2330
|
});
|
|
2094
2331
|
// The reply is untrusted: a cut-off or all-scratchpad summary must not be allowed
|
|
2095
2332
|
// to replace the conversation. See usableSummary.
|
|
@@ -2097,21 +2334,38 @@ async function autocompact(session, options) {
|
|
|
2097
2334
|
// is what keeps the meter honest: a turn that happened to trip the bar spends
|
|
2098
2335
|
// a whole extra summarisation call, and leaving that out made the figure short
|
|
2099
2336
|
// by exactly the work nobody could see.
|
|
2100
|
-
if (turn.usage)
|
|
2337
|
+
if (turn.usage) {
|
|
2101
2338
|
options.onEvent?.({ type: "usage", ...turn.usage });
|
|
2339
|
+
countUsage(session, turn.usage, options);
|
|
2340
|
+
}
|
|
2102
2341
|
const usable = usableSummary(turn.content, turn.stop);
|
|
2103
2342
|
if (!usable)
|
|
2104
2343
|
return void fail(turn.stop && turn.stop !== "end" ? `the summary came back ${turn.stop}` : "the summary was unusable");
|
|
2105
2344
|
summary = usable;
|
|
2106
2345
|
}
|
|
2107
2346
|
catch (error) {
|
|
2347
|
+
// Stopped by the user: not a failed compaction, so nothing to count or report.
|
|
2348
|
+
if (options.signal?.aborted)
|
|
2349
|
+
return;
|
|
2350
|
+
// Some free OpenRouter models gate on the CALLING APP's identity, not the shape of
|
|
2351
|
+
// any one request — real tool schemas (withAuxModel's retry, above) genuinely fix a
|
|
2352
|
+
// model that just needs a non-empty `tools` array, but this specific refusal was
|
|
2353
|
+
// confirmed live to survive that retry too: OpenRouter's own error names the cause
|
|
2354
|
+
// as `"failed_routing_step":"Gate Free Endpoints by Agentic Harness"`, which nothing
|
|
2355
|
+
// Mindweave sends in a request can satisfy. Say so plainly instead of surfacing the
|
|
2356
|
+
// provider's raw JSON, which reads as a Mindweave bug rather than a model limit.
|
|
2357
|
+
if (isAgenticOnlyRefusal(error)) {
|
|
2358
|
+
return void fail(`${session.modelConfig.model} won't serve Mindweave's background calls — some free OpenRouter models restrict ` +
|
|
2359
|
+
`themselves to specific recognised apps, and switching what a request sends can't change that. Pick a ` +
|
|
2360
|
+
`different model for this project, or expect compaction and session notes to stay off on this one.`);
|
|
2361
|
+
}
|
|
2108
2362
|
return void fail(providerMessage(detailOf(error)) || "the summarizer call failed");
|
|
2109
2363
|
}
|
|
2110
2364
|
// A summary replaces the transcript prefix, so file contents read before it are gone.
|
|
2111
2365
|
// Nothing re-injects them: the working-set block that used to do so was removed for
|
|
2112
2366
|
// costing up to 12K per model call. The model re-reads what it still needs, which
|
|
2113
2367
|
// read_file allows because the summary also clears the presence set the dedup checks.
|
|
2114
|
-
session
|
|
2368
|
+
replaceTranscript(session, spliceSummary(session.transcript, summary, KEEP_LAST_N));
|
|
2115
2369
|
// The notes no longer describe the transcript they were measured against, and the
|
|
2116
2370
|
// summary now covers everything before the kept tail.
|
|
2117
2371
|
session.sessionMemoryEntries = 1;
|
|
@@ -2130,6 +2384,12 @@ async function autocompact(session, options) {
|
|
|
2130
2384
|
async function finishCompaction(session, options, before) {
|
|
2131
2385
|
session.compactFailures = 0; // a clean compaction resets the breaker
|
|
2132
2386
|
await restoreAfterCompaction(session);
|
|
2387
|
+
// What is still running, said right after the summary: the summary is history, and
|
|
2388
|
+
// may not mention a dev server that is still up or an app still open for testing,
|
|
2389
|
+
// and a model that does not know starts them again.
|
|
2390
|
+
const live = liveStateNote(session.toolContext.backgroundShells?.running() ?? [], uiLiveState(session.toolContext));
|
|
2391
|
+
if (live)
|
|
2392
|
+
session.transcript.splice(1, 0, { role: "user", content: live, synthetic: true });
|
|
2133
2393
|
// Re-read the governor unconditionally here. The prompt is being rebuilt from scratch
|
|
2134
2394
|
// at this point, so it is the natural moment to rebuild what it is made of — and it is
|
|
2135
2395
|
// the one path that does not depend on the stat check being right about anything.
|
|
@@ -2153,6 +2413,27 @@ async function finishCompaction(session, options, before) {
|
|
|
2153
2413
|
// the moment to pick the file back up, and it costs nothing extra here.
|
|
2154
2414
|
await reloadProjectMemory(session).catch(() => { });
|
|
2155
2415
|
}
|
|
2416
|
+
/**
|
|
2417
|
+
* The note put after a compaction about what is still running, or null when nothing is
|
|
2418
|
+
* (pure). Background commands are listed with the ports they are listening on, so a
|
|
2419
|
+
* model that has lost the history does not start a second dev server; an app open for
|
|
2420
|
+
* testing is named, with the instruction to look before acting.
|
|
2421
|
+
*/
|
|
2422
|
+
export function liveStateNote(shells, ui, now = Date.now()) {
|
|
2423
|
+
const lines = [];
|
|
2424
|
+
for (const sh of shells) {
|
|
2425
|
+
const mins = Math.max(0, Math.round((now - sh.startedAt) / 60_000));
|
|
2426
|
+
const ports = sh.listening?.length ? sh.listening : sh.port ? [sh.port] : [];
|
|
2427
|
+
lines.push(`- Background shell #${sh.id} (\`${sh.command}\`): running for ${mins < 1 ? "under a minute" : `${mins} min`}` +
|
|
2428
|
+
`${ports.length ? `, listening on port ${ports.join(", ")}` : ""}.`);
|
|
2429
|
+
}
|
|
2430
|
+
if (ui)
|
|
2431
|
+
lines.push(`- ${ui}`);
|
|
2432
|
+
if (lines.length === 0)
|
|
2433
|
+
return null;
|
|
2434
|
+
return (`[Still running after the compaction. This is the live state now, not history.]\n${lines.join("\n")}\n` +
|
|
2435
|
+
`Do not start any of these again: they are already running. Use shells to check one, kill_shell to stop it.`);
|
|
2436
|
+
}
|
|
2156
2437
|
/**
|
|
2157
2438
|
* Reconcile the read ledger with the transcript, and put the working files back.
|
|
2158
2439
|
*
|
|
@@ -2179,7 +2460,7 @@ async function restoreAfterCompaction(session) {
|
|
|
2179
2460
|
// earlier fix copied every path forward to re-derive the same answer every step.
|
|
2180
2461
|
// The correctness half. Unconditional, and before anything that can throw.
|
|
2181
2462
|
reads.clear();
|
|
2182
|
-
const budget = restoreBudgetFor(
|
|
2463
|
+
const budget = restoreBudgetFor(effectiveAutoCompactThreshold(session));
|
|
2183
2464
|
if (budget <= 0)
|
|
2184
2465
|
return;
|
|
2185
2466
|
// What the kept tail still shows. Re-sending a file the model can already see costs
|