mindweave 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -145
- package/dist/cli/App.js +579 -304
- 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/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 +759 -0
- package/dist/core/turnRunner.js.map +1 -0
- package/dist/drivers/anthropic/client.js +101 -26
- package/dist/drivers/anthropic/client.js.map +1 -1
- package/dist/drivers/anthropic/manifest.js +43 -13
- package/dist/drivers/anthropic/manifest.js.map +1 -1
- package/dist/drivers/cerebras/manifest.js +13 -3
- package/dist/drivers/cerebras/manifest.js.map +1 -1
- package/dist/drivers/deepseek/manifest.js +24 -34
- package/dist/drivers/deepseek/manifest.js.map +1 -1
- package/dist/drivers/glm/manifest.js +21 -6
- 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/kimi/manifest.js +13 -10
- package/dist/drivers/kimi/manifest.js.map +1 -1
- package/dist/drivers/meta/manifest.js +21 -16
- package/dist/drivers/meta/manifest.js.map +1 -1
- package/dist/drivers/minimax/manifest.js +15 -12
- package/dist/drivers/minimax/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 +69 -18
- package/dist/drivers/openai/manifest.js.map +1 -1
- package/dist/drivers/qwen/manifest.js +19 -6
- package/dist/drivers/qwen/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/drivers/xai/client.js +12 -10
- package/dist/drivers/xai/client.js.map +1 -1
- package/dist/drivers/xai/manifest.js +62 -25
- package/dist/drivers/xai/manifest.js.map +1 -1
- package/dist/dynamo/auxModel.js +54 -0
- package/dist/dynamo/auxModel.js.map +1 -0
- package/dist/dynamo/engine.js +407 -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 +70 -15
- 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/detail.js +9 -6
- package/dist/tools/detail.js.map +1 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/edit.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/replaceSymbol.js +2 -1
- package/dist/tools/replaceSymbol.js.map +1 -1
- package/dist/tools/runCommand.js +113 -9
- 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/dist/tools/writeFile.js +2 -1
- package/dist/tools/writeFile.js.map +1 -1
- package/package.json +78 -75
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);
|
|
@@ -451,6 +471,12 @@ function workspaceText(session) {
|
|
|
451
471
|
function modelSeesImages(session) {
|
|
452
472
|
return manifestForModel(session.modelConfig.model).acceptsImages?.(session.modelConfig.model) ?? false;
|
|
453
473
|
}
|
|
474
|
+
/** "JPG and PNG" from `["image/png", "image/jpeg"]` — the names a user knows the files by. */
|
|
475
|
+
export function imageTypeNames(types) {
|
|
476
|
+
const names = types.map((t) => ({ "image/jpeg": "JPG", "image/png": "PNG", "image/gif": "GIF", "image/webp": "WebP" })[t] ?? t);
|
|
477
|
+
const sorted = [...new Set(names)].sort();
|
|
478
|
+
return sorted.length <= 1 ? (sorted[0] ?? "") : `${sorted.slice(0, -1).join(", ")} and ${sorted[sorted.length - 1]}`;
|
|
479
|
+
}
|
|
454
480
|
async function loadImagePayloads(session) {
|
|
455
481
|
// Nothing to load for a model that cannot look at one. This is the /provider switch
|
|
456
482
|
// case: a picture attached while a vision model was running stays in the transcript,
|
|
@@ -476,12 +502,19 @@ async function loadImagePayloads(session) {
|
|
|
476
502
|
}));
|
|
477
503
|
return out;
|
|
478
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)";
|
|
479
507
|
function buildRequest(session, tools, imagePayloads = new Map(),
|
|
480
508
|
/** Notes for the folders in play, resolved by the caller (it has to read disk). */
|
|
481
509
|
directoryNotes = []) {
|
|
482
510
|
const canSeeImages = modelSeesImages(session);
|
|
511
|
+
// The formats it takes, when narrower than everything core attaches. A manifest fact,
|
|
512
|
+
// like vision itself; absent means every type.
|
|
513
|
+
const imageTypes = canSeeImages
|
|
514
|
+
? manifestForModel(session.modelConfig.model).imageTypes?.(session.modelConfig.model)
|
|
515
|
+
: undefined;
|
|
483
516
|
const messages = [];
|
|
484
|
-
for (const e of session.transcript) {
|
|
517
|
+
for (const [i, e] of session.transcript.entries()) {
|
|
485
518
|
if (e.role === "user" || e.role === "summary") {
|
|
486
519
|
// Attached images ride with the message, but only while their payload is still
|
|
487
520
|
// live: microcompaction drops the refs once the turn is old, and a file deleted
|
|
@@ -491,11 +524,16 @@ directoryNotes = []) {
|
|
|
491
524
|
// A message that arrived mid-turn is framed HERE rather than being stored framed,
|
|
492
525
|
// so the transcript keeps what the person typed and only the wire carries the
|
|
493
526
|
// explanation. Deterministic from the entry, so the cached prefix is unaffected.
|
|
494
|
-
|
|
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;
|
|
495
532
|
if (refs && refs.length > 0) {
|
|
496
533
|
const images = [];
|
|
497
534
|
const missing = [];
|
|
498
535
|
const unseen = [];
|
|
536
|
+
const wrongType = [];
|
|
499
537
|
for (const ref of refs) {
|
|
500
538
|
// Told, never silently dropped. A message that mentions a screenshot and
|
|
501
539
|
// carries nothing reads to the model as a picture it failed to notice; the
|
|
@@ -504,6 +542,12 @@ directoryNotes = []) {
|
|
|
504
542
|
unseen.push(basename(ref.path));
|
|
505
543
|
continue;
|
|
506
544
|
}
|
|
545
|
+
// A format the provider would reject: held back the same way, so the model
|
|
546
|
+
// can tell the user which formats work instead of the whole request failing.
|
|
547
|
+
if (imageTypes && !imageTypes.includes(ref.mediaType)) {
|
|
548
|
+
wrongType.push(basename(ref.path));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
507
551
|
const data = imagePayloads.get(ref.path);
|
|
508
552
|
if (data)
|
|
509
553
|
images.push({ path: ref.path, mediaType: ref.mediaType, data });
|
|
@@ -514,10 +558,17 @@ directoryNotes = []) {
|
|
|
514
558
|
...(unseen.length > 0
|
|
515
559
|
? [`${unseen.join(", ")} was attached, but the model now running cannot see images`]
|
|
516
560
|
: []),
|
|
561
|
+
...(wrongType.length > 0 && imageTypes
|
|
562
|
+
? [
|
|
563
|
+
`${wrongType.join(", ")} was attached but not sent: the model now running ` +
|
|
564
|
+
`accepts only ${imageTypeNames(imageTypes)} images, so tell the user to ` +
|
|
565
|
+
`convert it or switch models`,
|
|
566
|
+
]
|
|
567
|
+
: []),
|
|
517
568
|
...(missing.length > 0 ? [`${missing.join(", ")} could not be read from disk`] : []),
|
|
518
569
|
];
|
|
519
|
-
const content = notes.length > 0 ? `${said}
|
|
520
|
-
|
|
570
|
+
const content = notes.length > 0 ? `${said}
|
|
571
|
+
|
|
521
572
|
[${notes.join("; ")}]` : said;
|
|
522
573
|
messages.push({ role: "user", content, ...(images.length > 0 ? { images } : {}) });
|
|
523
574
|
continue;
|
|
@@ -525,10 +576,15 @@ directoryNotes = []) {
|
|
|
525
576
|
messages.push({ role: "user", content: said });
|
|
526
577
|
}
|
|
527
578
|
else if (e.role === "assistant") {
|
|
579
|
+
const calls = e.toolCalls && e.toolCalls.length > 0;
|
|
528
580
|
messages.push({
|
|
529
581
|
role: "assistant",
|
|
530
|
-
|
|
531
|
-
|
|
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) } : {}),
|
|
532
588
|
});
|
|
533
589
|
}
|
|
534
590
|
else {
|
|
@@ -542,7 +598,7 @@ directoryNotes = []) {
|
|
|
542
598
|
// END of the system prompt. The end, because everything before it is the prefix every
|
|
543
599
|
// session shares and caches against; appending leaves that untouched.
|
|
544
600
|
const agentPrompt = session.toolContext.agentPrompt;
|
|
545
|
-
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()));
|
|
546
602
|
return {
|
|
547
603
|
system: agentPrompt ? `${base}\n\n${agentPrompt}` : base,
|
|
548
604
|
messages,
|
|
@@ -557,6 +613,11 @@ directoryNotes = []) {
|
|
|
557
613
|
// the command's own result says where it is now.
|
|
558
614
|
session.toolContext.cwdResetFrom && session.toolContext.cwd === session.cwd
|
|
559
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")
|
|
560
621
|
: ""),
|
|
561
622
|
tools,
|
|
562
623
|
model: session.modelConfig,
|
|
@@ -582,14 +643,27 @@ async function backgroundEventNotes(session) {
|
|
|
582
643
|
* app", which blamed the user for the agent's own restart and gave the model a second,
|
|
583
644
|
* contradictory account of the same event.
|
|
584
645
|
*/
|
|
585
|
-
export function backgroundEventNote({ info, kind, tail, wake, }) {
|
|
646
|
+
export function backgroundEventNote({ info, kind, tail, wake, ports, }) {
|
|
586
647
|
// It came up. This is the only positive event a server ever produces, and it is
|
|
587
648
|
// what lets the model actually deliver the "I'll tell you when it's running" it
|
|
588
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
|
+
}
|
|
589
661
|
if (kind === "ready") {
|
|
590
|
-
|
|
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` +
|
|
591
664
|
`Recent output:\n${tail || "(no output)"}\n\n` +
|
|
592
|
-
`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, ` +
|
|
593
667
|
`do not restart it, and do not change any files because of this. Running only means the ` +
|
|
594
668
|
`process started: do not describe what it shows or say a change is visible unless you ` +
|
|
595
669
|
`have actually looked.`);
|
|
@@ -677,7 +751,7 @@ export function callIsConcurrencySafe(tool, args) {
|
|
|
677
751
|
* an unreadable rules directory must not take the turn down with it — on any failure the
|
|
678
752
|
* session simply keeps the governance it already had.
|
|
679
753
|
*/
|
|
680
|
-
async function refreshGovernance(session, force = false) {
|
|
754
|
+
export async function refreshGovernance(session, force = false) {
|
|
681
755
|
try {
|
|
682
756
|
const stamp = await governanceStamp(session.toolContext.cwd);
|
|
683
757
|
// Skipping when the stamp is unchanged is what keeps our OWN writes from causing a
|
|
@@ -738,13 +812,13 @@ export async function respond(session, options = {}) {
|
|
|
738
812
|
function implementFromScratch(priorPath, plan) {
|
|
739
813
|
const path = priorPath;
|
|
740
814
|
const where = path
|
|
741
|
-
? `
|
|
742
|
-
|
|
815
|
+
? `
|
|
816
|
+
|
|
743
817
|
If you need something exact from the planning that produced this — a snippet, an ` +
|
|
744
818
|
`error message, a path — the full conversation is at: ${path}`
|
|
745
819
|
: "";
|
|
746
|
-
return `Implement the following plan:
|
|
747
|
-
|
|
820
|
+
return `Implement the following plan:
|
|
821
|
+
|
|
748
822
|
${plan}${where}`;
|
|
749
823
|
}
|
|
750
824
|
/**
|
|
@@ -781,11 +855,23 @@ async function respondTurn(session, options = {}) {
|
|
|
781
855
|
// every subsequent turn, and it keeps `activeDriver()` safe to call synchronously
|
|
782
856
|
// from here down (including from inside a tool).
|
|
783
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(() => { });
|
|
784
862
|
// Pick up a governance file the USER edited by hand since the last turn. One stat
|
|
785
863
|
// pass over a few small directories, taken here because a turn is the only moment
|
|
786
864
|
// governance is consulted — so it is fresh exactly where it is used, with no watcher
|
|
787
865
|
// to own, poll or tear down. See refreshGovernance.
|
|
788
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;
|
|
789
875
|
// Resume an approved plan from disk, once per session (undefined = unchecked).
|
|
790
876
|
// A plan approved last session is still the agreed scope this session — that is
|
|
791
877
|
// the point of it being an artifact — and the user deleting the file (or its
|
|
@@ -840,7 +926,13 @@ async function respondTurn(session, options = {}) {
|
|
|
840
926
|
// Session): fork a scoped child, forward the child's usage to this turn's meter,
|
|
841
927
|
// and share this turn's abort signal so Esc stops a sub-agent too.
|
|
842
928
|
session.toolContext.forkChild = (task, opts) => forkSession(session, task, opts);
|
|
843
|
-
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
|
+
};
|
|
844
936
|
// The raw event sink, so spawn_subagent can surface a child's nested activity
|
|
845
937
|
// (its lifecycle + tagged tool calls) up this same stream instead of running dark.
|
|
846
938
|
session.toolContext.emitEvent = options.onEvent;
|
|
@@ -916,6 +1008,7 @@ async function respondTurn(session, options = {}) {
|
|
|
916
1008
|
// Narration budget: one nudge per turn, and the turn's earlier prose to compare against.
|
|
917
1009
|
let narrationNudged = false;
|
|
918
1010
|
const narratedBefore = [];
|
|
1011
|
+
const readThisTurn = new Set(); // files read so far, for narrationShown
|
|
919
1012
|
// Judged next to the prose, pushed after the tool results — see the gate below.
|
|
920
1013
|
let pendingNarrationFault = null;
|
|
921
1014
|
let lastFailSig = null;
|
|
@@ -932,6 +1025,12 @@ async function respondTurn(session, options = {}) {
|
|
|
932
1025
|
// no matter how the turn ends (finish, pause, interrupt, throw). Labeled with
|
|
933
1026
|
// the request that drove it. No-op when nothing was edited.
|
|
934
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();
|
|
935
1034
|
// Fold this turn's cost into the session total on the way out, however the turn ends.
|
|
936
1035
|
// In the `finally` rather than the success path on purpose: an interrupted or failed
|
|
937
1036
|
// turn still spent the tokens it spent, and a spend figure that quietly omits the
|
|
@@ -968,7 +1067,7 @@ async function respondTurn(session, options = {}) {
|
|
|
968
1067
|
finally {
|
|
969
1068
|
recordSpend();
|
|
970
1069
|
const before = session.toolContext.checkpoints?.list().length ?? 0;
|
|
971
|
-
session.toolContext.checkpoints?.seal(turnLabel);
|
|
1070
|
+
session.toolContext.checkpoints?.seal(turnLabel, opener?.ts);
|
|
972
1071
|
// Say that a restore point exists. It was made silently, so `/undo` was a feature
|
|
973
1072
|
// you had to already know about — and the moment to learn it is the moment there is
|
|
974
1073
|
// something to undo, not after you have lost it.
|
|
@@ -982,11 +1081,16 @@ async function respondTurn(session, options = {}) {
|
|
|
982
1081
|
for (let step = 0; stepLimit === undefined || step < stepLimit; step++) {
|
|
983
1082
|
if (options.signal?.aborted)
|
|
984
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");
|
|
985
1089
|
// Stop before another (billable) call if a cost/time ceiling is hit — pause
|
|
986
1090
|
// losslessly, exactly like the step budget, so the user can raise it and resume.
|
|
987
1091
|
const limitReason = taskLimitReason(summarizeTask(usages, session.modelConfig.model), Date.now() - startedAt, limits);
|
|
988
1092
|
if (limitReason)
|
|
989
|
-
return pauseTask(session, options, `hit the ${limitReason}
|
|
1093
|
+
return pauseTask(session, options, `hit the ${limitReason}`, "costTimeLimit");
|
|
990
1094
|
await maybeCompact(session, options);
|
|
991
1095
|
// NO working-set block is built or sent. It used to be: the current contents of
|
|
992
1096
|
// every active file, rebuilt each step and injected at the tail — up to 12K tokens
|
|
@@ -1086,7 +1190,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1086
1190
|
if (!shed)
|
|
1087
1191
|
return false;
|
|
1088
1192
|
overflowRecovered = true;
|
|
1089
|
-
session
|
|
1193
|
+
replaceTranscript(session, shed);
|
|
1090
1194
|
await options.persist?.();
|
|
1091
1195
|
options.onActivity?.("conversation was too long — dropped the oldest turns and retried", {
|
|
1092
1196
|
context: true,
|
|
@@ -1119,6 +1223,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1119
1223
|
// may span several calls across tool rounds, and the UI sums them.
|
|
1120
1224
|
emitUsage(result, options);
|
|
1121
1225
|
if (result.usage) {
|
|
1226
|
+
countUsage(session, result.usage, options);
|
|
1122
1227
|
usages.push(result.usage);
|
|
1123
1228
|
usageTimes.push(Date.now());
|
|
1124
1229
|
writeCacheLog(cacheCallLine({
|
|
@@ -1155,7 +1260,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1155
1260
|
if (content.trim())
|
|
1156
1261
|
session.transcript.push({ role: "assistant", content });
|
|
1157
1262
|
await options.persist?.();
|
|
1158
|
-
return pauseTask(session, options, note);
|
|
1263
|
+
return pauseTask(session, options, note, result.stop);
|
|
1159
1264
|
}
|
|
1160
1265
|
// No tool calls → the model is done. Record the reply.
|
|
1161
1266
|
if (toolCalls.length === 0) {
|
|
@@ -1221,6 +1326,13 @@ async function respondTurn(session, options = {}) {
|
|
|
1221
1326
|
// Durable BEFORE running the tools: if the machine dies mid-tool, the resume path
|
|
1222
1327
|
// sees these dangling tool_calls and reconciles them (reconcileInterruptedTools).
|
|
1223
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
|
+
}
|
|
1224
1336
|
// Narration gate, part one: JUDGE here, where this message's prose and the turn's
|
|
1225
1337
|
// earlier prose are both in hand. Do NOT push anything yet — an assistant message
|
|
1226
1338
|
// carrying tool_calls must be followed immediately by a tool message per call, and
|
|
@@ -1234,11 +1346,20 @@ async function respondTurn(session, options = {}) {
|
|
|
1234
1346
|
// Announce every tool the model chose, in its order, BEFORE running any —
|
|
1235
1347
|
// the UI's reveal queue paces them and a slow tool (test/run) can show a live
|
|
1236
1348
|
// "running" state until its end event lands.
|
|
1237
|
-
|
|
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);
|
|
1238
1359
|
options.onEvent?.({ type: "tool", phase: "start", id: call.id, name: call.name, args: parseArgs(call.arguments) });
|
|
1239
|
-
}
|
|
1240
|
-
//
|
|
1241
|
-
// (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
|
|
1242
1363
|
// is concurrency-safe when the tool says so for THESE args (isConcurrencySafe) —
|
|
1243
1364
|
// e.g. a read-only sub-agent, which lets the model fan out research — otherwise
|
|
1244
1365
|
// the default is: read-only ⇒ safe, mutating ⇒ serial.
|
|
@@ -1246,8 +1367,9 @@ async function respondTurn(session, options = {}) {
|
|
|
1246
1367
|
const tool = lookup(call.name);
|
|
1247
1368
|
return tool ? callIsConcurrencySafe(tool, parseArgs(call.arguments)) : false;
|
|
1248
1369
|
};
|
|
1249
|
-
|
|
1250
|
-
|
|
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);
|
|
1251
1373
|
const runCall = async (call) => {
|
|
1252
1374
|
// Esc: once the turn is aborted, no further tool may START. The step loop only
|
|
1253
1375
|
// re-checks BETWEEN steps, so without this gate the rest of a batch still runs
|
|
@@ -1320,7 +1442,10 @@ async function respondTurn(session, options = {}) {
|
|
|
1320
1442
|
// uniformly — including subagent edits. Fails safe: no approval channel, or an
|
|
1321
1443
|
// unclear answer, refuses rather than runs.
|
|
1322
1444
|
const ctx = session.toolContext;
|
|
1323
|
-
|
|
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) {
|
|
1324
1449
|
const args = parseArgs(call.arguments);
|
|
1325
1450
|
// The question is one line; WHAT is about to happen rides as detail, which the
|
|
1326
1451
|
// CLI prints into the transcript. A gate the user cannot read is a gate they
|
|
@@ -1365,21 +1490,61 @@ async function respondTurn(session, options = {}) {
|
|
|
1365
1490
|
// Zero threw — every one returned an error result. So this catches nothing today
|
|
1366
1491
|
// and is deliberately defence in depth: it removes a single point of failure
|
|
1367
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
|
+
}
|
|
1368
1510
|
let result;
|
|
1369
1511
|
try {
|
|
1370
1512
|
// A per-call channel, so a tool that runs for minutes can say what it is doing.
|
|
1371
1513
|
// Scoped to THIS call rather than hung on the shared context, which every tool in
|
|
1372
1514
|
// the turn holds the same instance of — with two commands in flight there would be
|
|
1373
1515
|
// no way to tell whose output was whose.
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
+
}
|
|
1377
1531
|
}
|
|
1378
1532
|
catch (error) {
|
|
1379
|
-
// An abort is the user, not a fault
|
|
1380
|
-
//
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
+
}
|
|
1383
1548
|
return { call, ...toolFailureResult(call.name, error), detail: undefined };
|
|
1384
1549
|
}
|
|
1385
1550
|
return {
|
|
@@ -1388,10 +1553,13 @@ async function respondTurn(session, options = {}) {
|
|
|
1388
1553
|
summary: result.summary,
|
|
1389
1554
|
isError: result.isError,
|
|
1390
1555
|
detail: result.detail,
|
|
1556
|
+
detailFull: result.detailFull,
|
|
1391
1557
|
detailKind: result.detailKind,
|
|
1392
1558
|
quiet: result.quiet,
|
|
1393
1559
|
fullContentOf: result.fullContentOf,
|
|
1394
1560
|
images: result.images,
|
|
1561
|
+
web: result.web,
|
|
1562
|
+
ui: result.ui,
|
|
1395
1563
|
displayKind: result.displayKind,
|
|
1396
1564
|
displayName: result.displayName,
|
|
1397
1565
|
awaitsModel: result.awaitsModel,
|
|
@@ -1403,6 +1571,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1403
1571
|
// the model's call order (the sort below); only the UI events go out eagerly.
|
|
1404
1572
|
const runAndEmit = async (call) => {
|
|
1405
1573
|
const r = await runCall(call);
|
|
1574
|
+
announce(r.call); // one that never ran gets its start now, right before its end
|
|
1406
1575
|
options.onEvent?.({
|
|
1407
1576
|
type: "tool",
|
|
1408
1577
|
phase: "end",
|
|
@@ -1411,16 +1580,23 @@ async function respondTurn(session, options = {}) {
|
|
|
1411
1580
|
summary: r.summary ?? r.call.name,
|
|
1412
1581
|
error: r.isError ?? false,
|
|
1413
1582
|
detail: r.detail,
|
|
1583
|
+
...("detailFull" in r && r.detailFull ? { detailFull: r.detailFull } : {}),
|
|
1414
1584
|
...(r.detailKind ? { detailKind: r.detailKind } : {}),
|
|
1415
1585
|
...(r.quiet ? { quiet: true } : {}),
|
|
1416
1586
|
...(r.displayKind ? { displayKind: r.displayKind } : {}),
|
|
1417
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 } : {}),
|
|
1418
1591
|
});
|
|
1419
1592
|
return r;
|
|
1420
1593
|
};
|
|
1421
|
-
const results =
|
|
1422
|
-
for (const
|
|
1423
|
-
|
|
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]));
|
|
1424
1600
|
}
|
|
1425
1601
|
// Hand results back in the model's original call order (start events were emitted
|
|
1426
1602
|
// in that order too), no matter which lane each call ran in.
|
|
@@ -1465,6 +1641,9 @@ async function respondTurn(session, options = {}) {
|
|
|
1465
1641
|
...(result.quiet ? { quiet: true } : {}),
|
|
1466
1642
|
...(result.displayName ? { displayName: result.displayName } : {}),
|
|
1467
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 } : {}),
|
|
1468
1647
|
...(result.isError ? { isError: true } : {}),
|
|
1469
1648
|
// Presence, as recorded by the tool that knows: this result IS the whole
|
|
1470
1649
|
// content of that file. Not display — the presence derivation reads it.
|
|
@@ -1734,10 +1913,19 @@ async function respondTurn(session, options = {}) {
|
|
|
1734
1913
|
// transcript, task list, and working set are all intact, so telling Mindweave to
|
|
1735
1914
|
// continue resumes exactly here with nothing lost, and the user stays in control of
|
|
1736
1915
|
// the spend.
|
|
1737
|
-
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");
|
|
1738
1917
|
}
|
|
1739
1918
|
}
|
|
1740
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
|
+
}
|
|
1741
1929
|
function lastUserText(session) {
|
|
1742
1930
|
for (let i = session.transcript.length - 1; i >= 0; i--) {
|
|
1743
1931
|
const e = session.transcript[i];
|
|
@@ -1769,6 +1957,7 @@ function endTurnWith(session, options, msg) {
|
|
|
1769
1957
|
* one in the same turn (the re-scope guard) — a natural checkpoint to let the user
|
|
1770
1958
|
* steer instead of the model taking on scope it wasn't asked for. */
|
|
1771
1959
|
function pauseReScope(session, options) {
|
|
1960
|
+
options.onPause?.("reScope");
|
|
1772
1961
|
return endTurnWith(session, options, "(I've finished the task list for what you asked. I have ideas for taking it " +
|
|
1773
1962
|
"further, but I've stopped here so you can steer — rather than piling on new scope " +
|
|
1774
1963
|
'on my own. Tell me which direction you want, or say "keep going" to continue.)');
|
|
@@ -1778,6 +1967,7 @@ function pauseReScope(session, options) {
|
|
|
1778
1967
|
* to do but wait — end the turn cleanly instead of looping "still running" checks.
|
|
1779
1968
|
* Deliberately worded as a status line to the user, not a "paused" apology. */
|
|
1780
1969
|
function pauseForBackgroundPoll(session, options) {
|
|
1970
|
+
options.onPause?.("backgroundPoll");
|
|
1781
1971
|
return endTurnWith(session, options, "It's still running in the background. I'll stop checking and let you know as soon " +
|
|
1782
1972
|
"as it finishes — no need to keep watching.");
|
|
1783
1973
|
}
|
|
@@ -1786,13 +1976,22 @@ function pauseForBackgroundPoll(session, options) {
|
|
|
1786
1976
|
* count, and its real working directory, and it still hasn't moved — so hand the wheel
|
|
1787
1977
|
* to the user rather than spend more steps on it. */
|
|
1788
1978
|
function pauseForRepeatedFailure(session, options, errorOutput) {
|
|
1979
|
+
options.onPause?.("repeatedFailure");
|
|
1789
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 ` +
|
|
1790
1981
|
`stopped rather than retry the same thing again. The error was:\n\n${firstErrorLine(errorOutput)}\n\n` +
|
|
1791
1982
|
`Tell me how you'd like to proceed, or I can try a different approach.`);
|
|
1792
1983
|
}
|
|
1793
1984
|
/** Record and return a clean, lossless pause reply (well-formed transcript) when a
|
|
1794
|
-
* guard trips — step budget or a cost/time ceiling. Saying "continue" resumes.
|
|
1795
|
-
|
|
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
|
+
}
|
|
1796
1995
|
return endTurnWith(session, options, `(Paused — ${reason}. The task isn't finished, but nothing is lost: your progress, ` +
|
|
1797
1996
|
`edits, and task list are saved. Say "continue" to pick up exactly where I left off.)`);
|
|
1798
1997
|
}
|
|
@@ -1847,6 +2046,18 @@ export function toCallRecord(u, model, at = Date.now()) {
|
|
|
1847
2046
|
model,
|
|
1848
2047
|
};
|
|
1849
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
|
+
}
|
|
1850
2061
|
/** Report a turn's token usage to the UI, if the provider returned it. */
|
|
1851
2062
|
function emitUsage(result, options) {
|
|
1852
2063
|
if (result.usage) {
|
|
@@ -1881,7 +2092,7 @@ async function sweepSessionMemory(session, options) {
|
|
|
1881
2092
|
const grown = shouldUpdateSessionMemory(estimateEntriesTokens(session.transcript), session.sessionMemoryTokens ?? 0, session.sessionMemoryInit ?? false);
|
|
1882
2093
|
if (!grown)
|
|
1883
2094
|
return;
|
|
1884
|
-
await updateSessionMemory(session);
|
|
2095
|
+
await updateSessionMemory(session, options.signal);
|
|
1885
2096
|
await options.persist?.(); // durable: the notes sidecar is written by the persister
|
|
1886
2097
|
}
|
|
1887
2098
|
/**
|
|
@@ -1911,12 +2122,39 @@ export function contextUsed(session) {
|
|
|
1911
2122
|
: (session.toolContext.mcp?.estimatedTokens() ?? 0);
|
|
1912
2123
|
return estimateEntriesTokens(session.transcript) + overhead;
|
|
1913
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
|
+
}
|
|
1914
2151
|
async function maybeCompact(session, options) {
|
|
1915
2152
|
const model = session.modelConfig.model;
|
|
1916
|
-
// Model-anchored bars (env still
|
|
1917
|
-
// 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.
|
|
1918
2156
|
const microBar = envInt("MINDWEAVE_MICROCOMPACT_TOKENS", microCompactThreshold(model));
|
|
1919
|
-
const autoBar =
|
|
2157
|
+
const autoBar = effectiveAutoCompactThreshold(session);
|
|
1920
2158
|
// MCP tool schemas are sent on every turn but live OUTSIDE the transcript, so the bars
|
|
1921
2159
|
// could not see them: a 30K-token catalog meant the model was 30K deeper into its real
|
|
1922
2160
|
// context than this arithmetic believed, and every threshold fired that much too late.
|
|
@@ -2021,6 +2259,16 @@ export async function compactNow(session, options = {}) {
|
|
|
2021
2259
|
async function autocompact(session, options) {
|
|
2022
2260
|
if (session.transcript.length === 0)
|
|
2023
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) {
|
|
2024
2272
|
// Measured BEFORE the summarizer runs, with the same arithmetic the thresholds use,
|
|
2025
2273
|
// so the bar the user sees is the number the system actually acted on.
|
|
2026
2274
|
const before = contextUsed(session);
|
|
@@ -2042,9 +2290,9 @@ async function autocompact(session, options) {
|
|
|
2042
2290
|
// summarizer is about to be paid to produce. When they are current enough to cover
|
|
2043
2291
|
// the prefix being dropped, spending a model call buys something already owned.
|
|
2044
2292
|
// Declines rather than approximates: stale or empty notes fall through.
|
|
2045
|
-
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));
|
|
2046
2294
|
if (fromNotes) {
|
|
2047
|
-
session
|
|
2295
|
+
replaceTranscript(session, fromNotes.entries);
|
|
2048
2296
|
// The notes now describe everything before the tail they were spliced in front of.
|
|
2049
2297
|
session.sessionMemoryEntries = 1;
|
|
2050
2298
|
session.sessionMemoryTokens = estimateEntriesTokens(session.transcript);
|
|
@@ -2054,15 +2302,31 @@ async function autocompact(session, options) {
|
|
|
2054
2302
|
let summary;
|
|
2055
2303
|
try {
|
|
2056
2304
|
// Summaries don't need reasoning — use the chosen model with thinking off.
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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 });
|
|
2066
2330
|
});
|
|
2067
2331
|
// The reply is untrusted: a cut-off or all-scratchpad summary must not be allowed
|
|
2068
2332
|
// to replace the conversation. See usableSummary.
|
|
@@ -2070,21 +2334,38 @@ async function autocompact(session, options) {
|
|
|
2070
2334
|
// is what keeps the meter honest: a turn that happened to trip the bar spends
|
|
2071
2335
|
// a whole extra summarisation call, and leaving that out made the figure short
|
|
2072
2336
|
// by exactly the work nobody could see.
|
|
2073
|
-
if (turn.usage)
|
|
2337
|
+
if (turn.usage) {
|
|
2074
2338
|
options.onEvent?.({ type: "usage", ...turn.usage });
|
|
2339
|
+
countUsage(session, turn.usage, options);
|
|
2340
|
+
}
|
|
2075
2341
|
const usable = usableSummary(turn.content, turn.stop);
|
|
2076
2342
|
if (!usable)
|
|
2077
2343
|
return void fail(turn.stop && turn.stop !== "end" ? `the summary came back ${turn.stop}` : "the summary was unusable");
|
|
2078
2344
|
summary = usable;
|
|
2079
2345
|
}
|
|
2080
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
|
+
}
|
|
2081
2362
|
return void fail(providerMessage(detailOf(error)) || "the summarizer call failed");
|
|
2082
2363
|
}
|
|
2083
2364
|
// A summary replaces the transcript prefix, so file contents read before it are gone.
|
|
2084
2365
|
// Nothing re-injects them: the working-set block that used to do so was removed for
|
|
2085
2366
|
// costing up to 12K per model call. The model re-reads what it still needs, which
|
|
2086
2367
|
// read_file allows because the summary also clears the presence set the dedup checks.
|
|
2087
|
-
session
|
|
2368
|
+
replaceTranscript(session, spliceSummary(session.transcript, summary, KEEP_LAST_N));
|
|
2088
2369
|
// The notes no longer describe the transcript they were measured against, and the
|
|
2089
2370
|
// summary now covers everything before the kept tail.
|
|
2090
2371
|
session.sessionMemoryEntries = 1;
|
|
@@ -2103,6 +2384,12 @@ async function autocompact(session, options) {
|
|
|
2103
2384
|
async function finishCompaction(session, options, before) {
|
|
2104
2385
|
session.compactFailures = 0; // a clean compaction resets the breaker
|
|
2105
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 });
|
|
2106
2393
|
// Re-read the governor unconditionally here. The prompt is being rebuilt from scratch
|
|
2107
2394
|
// at this point, so it is the natural moment to rebuild what it is made of — and it is
|
|
2108
2395
|
// the one path that does not depend on the stat check being right about anything.
|
|
@@ -2126,6 +2413,27 @@ async function finishCompaction(session, options, before) {
|
|
|
2126
2413
|
// the moment to pick the file back up, and it costs nothing extra here.
|
|
2127
2414
|
await reloadProjectMemory(session).catch(() => { });
|
|
2128
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
|
+
}
|
|
2129
2437
|
/**
|
|
2130
2438
|
* Reconcile the read ledger with the transcript, and put the working files back.
|
|
2131
2439
|
*
|
|
@@ -2152,7 +2460,7 @@ async function restoreAfterCompaction(session) {
|
|
|
2152
2460
|
// earlier fix copied every path forward to re-derive the same answer every step.
|
|
2153
2461
|
// The correctness half. Unconditional, and before anything that can throw.
|
|
2154
2462
|
reads.clear();
|
|
2155
|
-
const budget = restoreBudgetFor(
|
|
2463
|
+
const budget = restoreBudgetFor(effectiveAutoCompactThreshold(session));
|
|
2156
2464
|
if (budget <= 0)
|
|
2157
2465
|
return;
|
|
2158
2466
|
// What the kept tail still shows. Re-sending a file the model can already see costs
|