mindweave 2.5.1 → 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.
Files changed (235) hide show
  1. package/README.md +50 -114
  2. package/dist/cli/App.js +578 -303
  3. package/dist/cli/App.js.map +1 -1
  4. package/dist/cli/analytics.js +27 -18
  5. package/dist/cli/analytics.js.map +1 -1
  6. package/dist/cli/attachments.js +34 -0
  7. package/dist/cli/attachments.js.map +1 -1
  8. package/dist/cli/bannerFit.js +70 -0
  9. package/dist/cli/bannerFit.js.map +1 -0
  10. package/dist/cli/bootstrap.js +41 -3
  11. package/dist/cli/bootstrap.js.map +1 -1
  12. package/dist/cli/commands.js +4 -1
  13. package/dist/cli/commands.js.map +1 -1
  14. package/dist/cli/components/ApprovalBox.js +3 -2
  15. package/dist/cli/components/ApprovalBox.js.map +1 -1
  16. package/dist/cli/components/BlockView.js +27 -6
  17. package/dist/cli/components/BlockView.js.map +1 -1
  18. package/dist/cli/components/FirstRunFrame.js +2 -1
  19. package/dist/cli/components/FirstRunFrame.js.map +1 -1
  20. package/dist/cli/components/KeyManager.js +5 -4
  21. package/dist/cli/components/KeyManager.js.map +1 -1
  22. package/dist/cli/components/KeySetup.js +29 -13
  23. package/dist/cli/components/KeySetup.js.map +1 -1
  24. package/dist/cli/components/MarathonBox.js +64 -0
  25. package/dist/cli/components/MarathonBox.js.map +1 -0
  26. package/dist/cli/components/McpMinitabs.js +8 -7
  27. package/dist/cli/components/McpMinitabs.js.map +1 -1
  28. package/dist/cli/components/Picker.js +4 -3
  29. package/dist/cli/components/Picker.js.map +1 -1
  30. package/dist/cli/components/PromptInput.js +47 -8
  31. package/dist/cli/components/PromptInput.js.map +1 -1
  32. package/dist/cli/components/PulseDot.js +42 -0
  33. package/dist/cli/components/PulseDot.js.map +1 -0
  34. package/dist/cli/components/SubagentView.js +9 -6
  35. package/dist/cli/components/SubagentView.js.map +1 -1
  36. package/dist/cli/components/TipLine.js +2 -1
  37. package/dist/cli/components/TipLine.js.map +1 -1
  38. package/dist/cli/components/ToolGroup.js +65 -49
  39. package/dist/cli/components/ToolGroup.js.map +1 -1
  40. package/dist/cli/components/ToolLine.js +20 -19
  41. package/dist/cli/components/ToolLine.js.map +1 -1
  42. package/dist/cli/components/TrustGate.js +2 -1
  43. package/dist/cli/components/TrustGate.js.map +1 -1
  44. package/dist/cli/components/minitabs.js +3 -2
  45. package/dist/cli/components/minitabs.js.map +1 -1
  46. package/dist/cli/desktopApp.js +45 -0
  47. package/dist/cli/desktopApp.js.map +1 -0
  48. package/dist/cli/help.js +1 -0
  49. package/dist/cli/help.js.map +1 -1
  50. package/dist/cli/inkOutputPatch.js +285 -0
  51. package/dist/cli/inkOutputPatch.js.map +1 -0
  52. package/dist/cli/inkSpeed.js +29 -0
  53. package/dist/cli/inkSpeed.js.map +1 -0
  54. package/dist/cli/inkSpeedLoader.js +47 -0
  55. package/dist/cli/inkSpeedLoader.js.map +1 -0
  56. package/dist/cli/inputText.js +30 -0
  57. package/dist/cli/inputText.js.map +1 -0
  58. package/dist/cli/keyManager.js +2 -1
  59. package/dist/cli/keyManager.js.map +1 -1
  60. package/dist/cli/keyPrefs.js +90 -0
  61. package/dist/cli/keyPrefs.js.map +1 -0
  62. package/dist/cli/keySetup.js +5 -2
  63. package/dist/cli/keySetup.js.map +1 -1
  64. package/dist/cli/marathonUi.js +113 -0
  65. package/dist/cli/marathonUi.js.map +1 -0
  66. package/dist/cli/markdown.js +63 -11
  67. package/dist/cli/markdown.js.map +1 -1
  68. package/dist/cli/modes.js +3 -3
  69. package/dist/cli/modes.js.map +1 -1
  70. package/dist/cli/prodEnv.js +43 -0
  71. package/dist/cli/prodEnv.js.map +1 -0
  72. package/dist/cli/revealPace.js +5 -5
  73. package/dist/cli/revealPace.js.map +1 -1
  74. package/dist/cli/revealQueue.js +120 -0
  75. package/dist/cli/revealQueue.js.map +1 -0
  76. package/dist/cli/theme.js +47 -0
  77. package/dist/cli/theme.js.map +1 -0
  78. package/dist/cli/toolDisplay.js +112 -18
  79. package/dist/cli/toolDisplay.js.map +1 -1
  80. package/dist/cli/transcript.js +1 -1
  81. package/dist/cli/transcript.js.map +1 -1
  82. package/dist/cli/virtualWindow.js +14 -2
  83. package/dist/cli/virtualWindow.js.map +1 -1
  84. package/dist/cli/wheelAccel.js +73 -0
  85. package/dist/cli/wheelAccel.js.map +1 -0
  86. package/dist/cli/widestLinePatch.js +48 -0
  87. package/dist/cli/widestLinePatch.js.map +1 -0
  88. package/dist/cli/workingVerb.js +88 -28
  89. package/dist/cli/workingVerb.js.map +1 -1
  90. package/dist/cli/wrap.js +1 -1
  91. package/dist/cli/wrap.js.map +1 -1
  92. package/dist/core/contextSettings.js +84 -0
  93. package/dist/core/contextSettings.js.map +1 -0
  94. package/dist/core/limitsView.js +40 -0
  95. package/dist/core/limitsView.js.map +1 -0
  96. package/dist/core/mcpServers.js +164 -0
  97. package/dist/core/mcpServers.js.map +1 -0
  98. package/dist/core/permissions.js +209 -0
  99. package/dist/core/permissions.js.map +1 -0
  100. package/dist/core/providerKeys.js +187 -0
  101. package/dist/core/providerKeys.js.map +1 -0
  102. package/dist/core/rulesSkills.js +142 -0
  103. package/dist/core/rulesSkills.js.map +1 -0
  104. package/dist/core/spendView.js +90 -0
  105. package/dist/core/spendView.js.map +1 -0
  106. package/dist/core/turnRunner.js +453 -60
  107. package/dist/core/turnRunner.js.map +1 -1
  108. package/dist/drivers/anthropic/client.js +24 -5
  109. package/dist/drivers/anthropic/client.js.map +1 -1
  110. package/dist/drivers/anthropic/manifest.js +15 -2
  111. package/dist/drivers/anthropic/manifest.js.map +1 -1
  112. package/dist/drivers/cerebras/manifest.js +6 -2
  113. package/dist/drivers/cerebras/manifest.js.map +1 -1
  114. package/dist/drivers/deepseek/manifest.js +12 -3
  115. package/dist/drivers/deepseek/manifest.js.map +1 -1
  116. package/dist/drivers/glm/manifest.js +3 -1
  117. package/dist/drivers/glm/manifest.js.map +1 -1
  118. package/dist/drivers/groq/manifest.js +5 -2
  119. package/dist/drivers/groq/manifest.js.map +1 -1
  120. package/dist/drivers/ollama/catalog.js +71 -0
  121. package/dist/drivers/ollama/catalog.js.map +1 -0
  122. package/dist/drivers/ollama/client.js +164 -0
  123. package/dist/drivers/ollama/client.js.map +1 -0
  124. package/dist/drivers/ollama/endpoint.js +42 -0
  125. package/dist/drivers/ollama/endpoint.js.map +1 -0
  126. package/dist/drivers/ollama/index.js +5 -0
  127. package/dist/drivers/ollama/index.js.map +1 -0
  128. package/dist/drivers/ollama/manifest.js +64 -0
  129. package/dist/drivers/ollama/manifest.js.map +1 -0
  130. package/dist/drivers/openai/client.js +10 -4
  131. package/dist/drivers/openai/client.js.map +1 -1
  132. package/dist/drivers/openai/manifest.js +46 -12
  133. package/dist/drivers/openai/manifest.js.map +1 -1
  134. package/dist/drivers/registry.js +11 -0
  135. package/dist/drivers/registry.js.map +1 -1
  136. package/dist/drivers/retryPolicy.js +26 -1
  137. package/dist/drivers/retryPolicy.js.map +1 -1
  138. package/dist/dynamo/auxModel.js +54 -0
  139. package/dist/dynamo/auxModel.js.map +1 -0
  140. package/dist/dynamo/engine.js +380 -99
  141. package/dist/dynamo/engine.js.map +1 -1
  142. package/dist/dynamo/guard.js +10 -0
  143. package/dist/dynamo/guard.js.map +1 -1
  144. package/dist/dynamo/liveMeter.js +25 -6
  145. package/dist/dynamo/liveMeter.js.map +1 -1
  146. package/dist/dynamo/marathon.js +409 -0
  147. package/dist/dynamo/marathon.js.map +1 -0
  148. package/dist/dynamo/marathonPrompt.js +45 -0
  149. package/dist/dynamo/marathonPrompt.js.map +1 -0
  150. package/dist/dynamo/model.js +6 -2
  151. package/dist/dynamo/model.js.map +1 -1
  152. package/dist/dynamo/toolBatches.js +58 -0
  153. package/dist/dynamo/toolBatches.js.map +1 -0
  154. package/dist/dynamo/usageLimits.js +446 -0
  155. package/dist/dynamo/usageLimits.js.map +1 -0
  156. package/dist/governor/freshness.js +10 -7
  157. package/dist/governor/freshness.js.map +1 -1
  158. package/dist/governor/index.js +76 -9
  159. package/dist/governor/index.js.map +1 -1
  160. package/dist/governor/write.js +43 -14
  161. package/dist/governor/write.js.map +1 -1
  162. package/dist/index.js +18 -151
  163. package/dist/index.js.map +1 -1
  164. package/dist/main.js +162 -0
  165. package/dist/main.js.map +1 -0
  166. package/dist/mcp/manager.js +18 -0
  167. package/dist/mcp/manager.js.map +1 -1
  168. package/dist/memory/allMetas.js +69 -0
  169. package/dist/memory/allMetas.js.map +1 -0
  170. package/dist/memory/compaction.js +69 -14
  171. package/dist/memory/compaction.js.map +1 -1
  172. package/dist/memory/earlier.js +15 -0
  173. package/dist/memory/earlier.js.map +1 -0
  174. package/dist/memory/profile.js +93 -0
  175. package/dist/memory/profile.js.map +1 -0
  176. package/dist/memory/rewind.js +262 -0
  177. package/dist/memory/rewind.js.map +1 -0
  178. package/dist/memory/session.js +42 -10
  179. package/dist/memory/session.js.map +1 -1
  180. package/dist/memory/sessionMemory.js +48 -13
  181. package/dist/memory/sessionMemory.js.map +1 -1
  182. package/dist/memory/sessionMemoryCompact.js +1 -1
  183. package/dist/memory/sessionMemoryCompact.js.map +1 -1
  184. package/dist/memory/sessionTitle.js +50 -0
  185. package/dist/memory/sessionTitle.js.map +1 -0
  186. package/dist/memory/store.js +42 -1
  187. package/dist/memory/store.js.map +1 -1
  188. package/dist/memory/types.js.map +1 -1
  189. package/dist/tools/askUser.js +14 -0
  190. package/dist/tools/askUser.js.map +1 -1
  191. package/dist/tools/backgroundShells.js +208 -5
  192. package/dist/tools/backgroundShells.js.map +1 -1
  193. package/dist/tools/browser.js +176 -0
  194. package/dist/tools/browser.js.map +1 -0
  195. package/dist/tools/cdp.js +165 -0
  196. package/dist/tools/cdp.js.map +1 -0
  197. package/dist/tools/checkpoints.js +208 -5
  198. package/dist/tools/checkpoints.js.map +1 -1
  199. package/dist/tools/deferredNative.js +2 -0
  200. package/dist/tools/deferredNative.js.map +1 -1
  201. package/dist/tools/exitPlan.js +4 -0
  202. package/dist/tools/exitPlan.js.map +1 -1
  203. package/dist/tools/hiddenDesktop.js +273 -0
  204. package/dist/tools/hiddenDesktop.js.map +1 -0
  205. package/dist/tools/listeningPorts.js +134 -0
  206. package/dist/tools/listeningPorts.js.map +1 -0
  207. package/dist/tools/mcpSearch.js +8 -0
  208. package/dist/tools/mcpSearch.js.map +1 -1
  209. package/dist/tools/mindweaveStatus.js +1 -1
  210. package/dist/tools/mindweaveStatus.js.map +1 -1
  211. package/dist/tools/registry.js +3 -0
  212. package/dist/tools/registry.js.map +1 -1
  213. package/dist/tools/runCommand.js +107 -6
  214. package/dist/tools/runCommand.js.map +1 -1
  215. package/dist/tools/screenshot.js +4 -6
  216. package/dist/tools/screenshot.js.map +1 -1
  217. package/dist/tools/screenshotWin.js +26 -9
  218. package/dist/tools/screenshotWin.js.map +1 -1
  219. package/dist/tools/stateCheckpoint.js +128 -0
  220. package/dist/tools/stateCheckpoint.js.map +1 -0
  221. package/dist/tools/todo.js +6 -0
  222. package/dist/tools/todo.js.map +1 -1
  223. package/dist/tools/ui.js +1295 -0
  224. package/dist/tools/ui.js.map +1 -0
  225. package/dist/tools/uiHide.js +356 -0
  226. package/dist/tools/uiHide.js.map +1 -0
  227. package/dist/tools/uiPage.js +861 -0
  228. package/dist/tools/uiPage.js.map +1 -0
  229. package/dist/tools/uiWin.js +506 -0
  230. package/dist/tools/uiWin.js.map +1 -0
  231. package/dist/tools/webFetch.js +32 -25
  232. package/dist/tools/webFetch.js.map +1 -1
  233. package/dist/tools/webSearch.js +12 -0
  234. package/dist/tools/webSearch.js.map +1 -1
  235. package/package.json +78 -78
@@ -10,19 +10,35 @@
10
10
  * so a tool call reads the same way in every surface) with no Ink, no pacing, no
11
11
  * terminal assumptions. A GUI or a server can drive a session with just this file.
12
12
  */
13
- import { respond } from "../dynamo/engine.js";
13
+ import { compactNow, respond } from "../dynamo/engine.js";
14
+ import { startMarathon, resumeMarathon, clearMarathon, describeMarathonEvent, verifyGoal, } from "../dynamo/marathon.js";
15
+ export { clearMarathon, describeMarathonEvent, MAX_MARATHON_VERIFY_FAILS } from "../dynamo/marathon.js";
16
+ /** How full the context is against the auto-compaction bar, for a context meter. */
17
+ export { contextFill } from "../dynamo/engine.js";
14
18
  import { createSession, resumeSession, reloadProjectMemory } from "../memory/session.js";
15
19
  import { saveSession, listSessions as listSessionsRaw } from "../memory/store.js";
16
- import { toolDisplay, isGroupable } from "../cli/toolDisplay.js";
17
- import { accessRefusal, providerOutage } from "../drivers/providerError.js";
20
+ import { toolDisplay, isGroupable, narrationShown, noteReads } from "../cli/toolDisplay.js";
21
+ import { accessRefusal, providerOutage, statusOf } from "../drivers/providerError.js";
22
+ import { failoverKey, noteKeyWorked } from "./providerKeys.js";
18
23
  import { providerOf, modelLabel, refreshModels, DISCOVERY_TTL_MS, withModel, saveModelConfig, thinkLevels, thinkLabel } from "../dynamo/model.js";
19
24
  import { allProviders, modelsOf } from "../drivers/registry.js";
20
25
  import { errText } from "../tools/editTarget.js";
21
26
  import { loadConfig, hasApiKey, saveApiKey, removeApiKey } from "../cli/bootstrap.js";
22
27
  import { describeImage, isRejection } from "../memory/images.js";
23
- import { stat } from "node:fs/promises";
28
+ import { resolveAttachments, stripAttachments, attachedFiles, hideAttachedNames } from "../cli/attachments.js";
29
+ export { attachedFiles, hideAttachedNames } from "../cli/attachments.js";
30
+ import { isRewindPoint } from "../memory/rewind.js";
31
+ export { rewindPoints, rewindTo, pasteSlot } from "../memory/rewind.js";
32
+ import { wrapPastedText, collapsePastes } from "../memory/pastedText.js";
33
+ import { manifestForModel } from "../drivers/registry.js";
34
+ import { stat, readFile } from "node:fs/promises";
35
+ import { join } from "node:path";
36
+ import { NOTES_FILE } from "../memory/projectNotes.js";
37
+ import { writeFileAtomic } from "../tools/atomicWrite.js";
24
38
  import { RECAP_STUB } from "../memory/compaction.js";
25
39
  import { stopChassis } from "../alternator/lane.js";
40
+ import { stopUi } from "../tools/ui.js";
41
+ import { runCommand } from "../tools/runCommand.js";
26
42
  import { APPROVAL_TEXT, APPROVAL_DISMISSED } from "../tools/approval.js";
27
43
  import { MODES, DEFAULT_MODE, modeById, modeFromFlags } from "../cli/modes.js";
28
44
  /**
@@ -38,12 +54,73 @@ export { MODES, DEFAULT_MODE, modeById, modeFromFlags };
38
54
  * resolve to `APPROVAL_DISMISSED` exactly, never an empty string or a thrown error.
39
55
  */
40
56
  export { APPROVAL_TEXT, APPROVAL_DISMISSED };
57
+ /** The typed-answer row exit_plan offers. A front end compares against it to know an
58
+ * approval is a PLAN (shown in full) rather than a question, without matching prose. */
59
+ export { PLAN_FEEDBACK } from "../tools/exitPlan.js";
60
+ /** The user's machine-wide profile (what to call them), shared with the CLI. */
61
+ export { readProfile, saveProfile } from "../memory/profile.js";
62
+ /** Feedback to the maintainer: the CLI's /feedback, same payload, same checks. */
63
+ export { buildFeedback, refuseReason, sendFeedback, ISSUES_URL, MAX_MESSAGE } from "../cli/feedback.js";
64
+ /** The anonymous usage count: the same id, on/off flag and ping the CLI uses. */
65
+ export { analyticsEnabled, setAnalyticsEnabled, sendAnalyticsPing } from "../cli/analytics.js";
66
+ export { appVersion } from "../cli/version.js";
67
+ /** Permissions: protected files, blocked commands and MCP tools, Sentinel allowances,
68
+ * default mode — per project or for every project. */
69
+ export { permissionsView, addPermission, removePermission, movePermission, revokeSessionGrant, setDefaultMode, defaultModeFor, } from "./permissions.js";
70
+ /** The user's own auto-compaction bar: this project's, all-projects', or Mindweave's
71
+ * own model-anchored default (with a one-line reason why it's what it is). */
72
+ export { contextView, contextRecommendationFor, setContextOverride, resetContextOverride, CONTEXT_PRESETS, CONTEXT_MIN_TOKENS, } from "./contextSettings.js";
73
+ /** Tokens spent so far, across every project this machine has state for — by model
74
+ * and by day/week/month. Read-only; tokens only, no dollar figure. */
75
+ export { spendView } from "./spendView.js";
76
+ export { limitsView, saveLimitsFromPanel, analyzeForPanel } from "./limitsView.js";
77
+ /** Rules and skills as files, for this project or all projects. */
78
+ export { listRulesSkills, readRuleSkill, saveRuleSkill, createRuleSkill, deleteRuleSkill, moveRuleSkill, importRuleSkill, } from "./rulesSkills.js";
79
+ /** MCP servers: list, add/edit, remove, enable/disable, reconnect, sign in/out. */
80
+ export { listMcpServers, mcpServerDetail, saveMcpServer, removeMcpServer, setMcpServerDisabled, reconnectMcpServer, signInMcpServer, signOutMcpServer, allowChangedMcpTools, onMcpChange, } from "./mcpServers.js";
81
+ /** Several keys per provider: add, edit, name, switch off, make default, auto-switch. */
82
+ export { providerKeys, addProviderKey, editProviderKey, removeProviderKey, useProviderKey, makeDefaultProviderKey, renameProviderKey, setProviderKeyDisabled, setProviderAutoSwitch, } from "./providerKeys.js";
41
83
  /**
42
84
  * Load the API key(s) into process.env — the same global `~/.mindweave/.env` /
43
85
  * project `.env` / shell-env layering the CLI uses (see cli/bootstrap.ts). Call
44
86
  * this once, at startup, before the first `startSession`.
45
87
  */
46
88
  export { loadConfig };
89
+ /**
90
+ * One provider's models with what each costs and can do, for a detail screen. Read from
91
+ * the same manifests the engine bills and routes by, so the screen can never show a
92
+ * price the cost math does not use. Null for an unknown provider id.
93
+ */
94
+ export function providerDetail(id) {
95
+ const p = allProviders().find((m) => m.id === id);
96
+ if (!p)
97
+ return null;
98
+ return {
99
+ id: p.id,
100
+ label: p.label,
101
+ apiKeyEnv: p.apiKeyEnv,
102
+ keysUrl: p.keysUrl,
103
+ connected: hasApiKey(p.apiKeyEnv),
104
+ local: !!p.local,
105
+ models: modelsOf(p).map((m) => {
106
+ const manifest = manifestForModel(m.id);
107
+ const price = manifest.price(m.id);
108
+ return {
109
+ id: m.id,
110
+ label: m.label,
111
+ description: m.description,
112
+ price: {
113
+ input: price.cacheMiss,
114
+ output: price.output,
115
+ cacheRead: price.cacheHit,
116
+ cacheWrite: price.cacheWrite ?? null,
117
+ },
118
+ acceptsImages: manifest.acceptsImages?.(m.id) ?? false,
119
+ thinks: manifest.thinkLevels(m.id).some((l) => l.thinking),
120
+ };
121
+ }),
122
+ };
123
+ }
47
124
  /**
48
125
  * Every installed provider — the real registry (drivers/registry.ts), not a
49
126
  * decorative list — with whether a key is already on file for it and the
@@ -57,6 +134,7 @@ export function providerSummaries() {
57
134
  apiKeyEnv: p.apiKeyEnv,
58
135
  keysUrl: p.keysUrl,
59
136
  connected: hasApiKey(p.apiKeyEnv),
137
+ local: !!p.local,
60
138
  models: modelsOf(p).map((m) => ({ id: m.id, label: m.label, description: m.description })),
61
139
  }));
62
140
  }
@@ -164,31 +242,59 @@ function splitExpiredImages(content) {
164
242
  */
165
243
  export function replayHistory(session) {
166
244
  const calls = new Map();
245
+ const readThisTurn = new Set(); // for narrationShown, as live
167
246
  const out = [];
168
- for (const entry of session.transcript) {
247
+ // What compaction took out comes first, so the chat shows the whole conversation.
248
+ // None of it can be rewound to: it is no longer part of the conversation.
249
+ const earlier = new Set(session.earlier ?? []);
250
+ for (const entry of [...earlier, ...session.transcript]) {
169
251
  if (entry.role === "user") {
170
252
  if (!entry.synthetic)
171
- out.push({ type: "userMessage", ...splitExpiredImages(entry.content) });
253
+ readThisTurn.clear();
254
+ // Shown as typed: file bodies hidden and pastes collapsed, the same as it looked live.
255
+ if (!entry.synthetic) {
256
+ const paths = (entry.images ?? []).map((i) => i.path);
257
+ const files = attachedFiles(entry.content, session.cwd);
258
+ const shown = splitExpiredImages(displayText(entry.content));
259
+ out.push({
260
+ ...shown,
261
+ type: "userMessage",
262
+ text: hideAttachedNames(shown.text, [...paths, ...files]),
263
+ ...(paths.length ? { images: paths } : {}),
264
+ ...(files.length ? { files } : {}),
265
+ // Typed mid-turn, or from before messages were stamped: nothing to go back to.
266
+ ...(isRewindPoint(entry) && !earlier.has(entry) ? {} : { noRewind: true }),
267
+ });
268
+ }
172
269
  }
173
270
  else if (entry.role === "assistant") {
271
+ // Words that led only to unseen tools were left out live, and are left out here too.
272
+ const stepCalls = (entry.toolCalls ?? []).map((c) => ({ name: c.name, args: safeParseArgs(c.arguments) }));
273
+ const shown = stepCalls.length === 0 || narrationShown(stepCalls, readThisTurn);
274
+ noteReads(stepCalls, readThisTurn);
174
275
  // RECAP_STUB is compaction's note to the MODEL that an old status reply was
175
276
  // condensed; it is not something the agent said, so it is never shown.
176
- if (entry.content.trim() && entry.content !== RECAP_STUB)
277
+ if (shown && entry.content.trim() && entry.content !== RECAP_STUB)
177
278
  out.push({ type: "assistantMessage", text: entry.content });
178
279
  for (const call of entry.toolCalls ?? [])
179
280
  calls.set(call.id, call);
180
281
  }
181
282
  else if (entry.role === "tool") {
182
283
  const call = calls.get(entry.toolCallId);
183
- if (entry.quiet)
284
+ // Quiet results, and loading a tool even from sessions saved before it was quiet. A search
285
+ // is quiet too, but it is still replayed (marked quiet): a front end may list what was searched.
286
+ if (call?.name === "find_tools")
287
+ continue;
288
+ if (entry.quiet && call?.name !== "search")
184
289
  continue;
185
290
  // The same display name a live toolStart carries ("Run", "Read"), not the raw
186
291
  // tool id — a reopened session was showing `run_command` where the live one said Run.
187
292
  let name = entry.displayName ?? call?.name ?? "tool";
188
293
  let arg;
189
294
  let kind = entry.displayKind ?? "meta";
295
+ const callArgs = call ? safeParseArgs(call.arguments) : undefined;
190
296
  if (call) {
191
- const d = toolDisplay(call.name, safeParseArgs(call.arguments));
297
+ const d = toolDisplay(call.name, callArgs ?? {});
192
298
  if (!entry.displayName)
193
299
  name = d.name;
194
300
  arg = d.arg;
@@ -198,12 +304,17 @@ export function replayHistory(session) {
198
304
  out.push({
199
305
  type: "toolReplay",
200
306
  name,
307
+ ...(call ? { tool: call.name, args: clipArgs(callArgs) } : {}),
201
308
  arg,
202
309
  kind,
203
310
  ok: !entry.isError,
204
311
  summary: entry.summary ?? entry.content,
205
312
  detail: entry.detail,
206
313
  detailKind: entry.detailKind,
314
+ ...(entry.imagePaths?.length ? { images: entry.imagePaths } : {}),
315
+ ...(entry.web ? { web: entry.web } : {}),
316
+ ...(entry.ui ? { ui: entry.ui } : {}),
317
+ ...(entry.quiet ? { quiet: true } : {}),
207
318
  });
208
319
  }
209
320
  else if (entry.role === "summary") {
@@ -256,6 +367,20 @@ export function attachHandlers(session, handlers) {
256
367
  session.toolContext.interrupt = handlers.interrupt;
257
368
  if (handlers.onModeChange)
258
369
  session.toolContext.onModeChange = handlers.onModeChange;
370
+ if (handlers.onLive)
371
+ session.toolContext.onLive = handlers.onLive;
372
+ }
373
+ /**
374
+ * Esc: stop everything the agent has running. The caller aborts the turn itself; this
375
+ * stops what outlives a turn: every background command (and so any app one launched)
376
+ * and the app being tested with its live view. Background commands are stopped as by
377
+ * the USER, so the model is told they were stopped rather than woken to restart them.
378
+ */
379
+ export async function stopEverything(session) {
380
+ const mgr = session.toolContext.backgroundShells;
381
+ for (const sh of mgr?.running() ?? [])
382
+ mgr?.kill(sh.id, "user");
383
+ await stopUi(session.toolContext);
259
384
  }
260
385
  /** The mode a session's CURRENT flags amount to — read after load, or after
261
386
  * `onModeChange` fires, to show the UI what's actually active. */
@@ -282,13 +407,78 @@ export function applySessionMode(session, id) {
282
407
  */
283
408
  export async function runTurn(session, message, handlers) {
284
409
  await reloadProjectMemory(session).catch(() => { });
285
- const images = await resolveImages(message.imagePaths ?? [], handlers.onEvent);
410
+ const images = [...(message.images ?? []), ...(await resolveImages(message.imagePaths ?? [], handlers.onEvent))];
411
+ await recordUserMessage(session, message.content, images, message.arrival);
412
+ await streamTurn(session, handlers);
413
+ }
414
+ /**
415
+ * Adds the user's message to the conversation and writes it to disk at once, before any model
416
+ * call. The first save of a turn used to come after the model's first step, so a kill or a crash
417
+ * during a slow first answer lost the message the user had just sent.
418
+ */
419
+ export async function recordUserMessage(session, content, images = [], arrival) {
286
420
  session.transcript.push({
287
421
  role: "user",
288
- content: message.content,
422
+ content,
423
+ // Sent by stopping the turn that was running (the app's "Send now"): the model is
424
+ // told it was stopped on purpose, as the CLI tells it after Esc.
425
+ ...(arrival ? { arrival } : {}),
289
426
  ...(images.length > 0 ? { images } : {}),
290
427
  });
291
- await streamTurn(session, handlers);
428
+ await saveSession(session);
429
+ }
430
+ /**
431
+ * A reply that was being written when the app died. A front end that kept the streamed words
432
+ * hands them here on the next start; they join the conversation, marked as cut off, so the
433
+ * user (and the model) can see what had been said. Nothing is added if a saved reply already
434
+ * holds those words: the kill may have come just after the step was saved.
435
+ */
436
+ export async function appendInterruptedReply(session, text) {
437
+ const words = text.trim();
438
+ if (!words)
439
+ return false;
440
+ const recent = session.transcript.slice(-4).filter((e) => e.role === "assistant" && typeof e.content === "string").map((e) => e.content.trim());
441
+ if (recent.some((c) => c && (c.includes(words.slice(0, 160)) || words.startsWith(c))))
442
+ return false;
443
+ session.transcript.push({ role: "assistant", content: `${words}\n\n(interrupted)` });
444
+ await saveSession(session);
445
+ return true;
446
+ }
447
+ /**
448
+ * What was typed, plus files added through the UI and long pastes, turned into what the
449
+ * model gets and what the chat shows. Shared by a fresh send and a message queued into a
450
+ * running turn, so the two can't resolve the same message differently.
451
+ *
452
+ * Files go through the CLI's own `resolveAttachments`, exactly as if they had been
453
+ * dropped into the prompt as quoted paths: images take the vision path when the running
454
+ * model can see them, text files arrive as `<attached_file>` blocks, binaries are skipped
455
+ * with a note. Pastes are wrapped with the CLI's `wrapPastedText`. So a message reads the
456
+ * same to the model whichever front end it was typed in.
457
+ */
458
+ export async function prepareMessage(session, input, onEvent) {
459
+ const paths = input.filePaths ?? [];
460
+ const withPaths = paths.length
461
+ ? `${input.text}${input.text ? "\n" : ""}${paths.map((p) => `"${p}"`).join(" ")}`
462
+ : input.text;
463
+ const model = session.modelConfig.model;
464
+ const canSee = manifestForModel(model).acceptsImages?.(model) ?? false;
465
+ const resolved = await resolveAttachments(withPaths, session.cwd, canSee);
466
+ // Only what went wrong is news; "attached a.ts (+40 lines)" is already on screen as a chip.
467
+ for (const note of resolved.notes) {
468
+ if (/^skipped |can't see images/.test(note))
469
+ onEvent({ type: "activity", line: note, error: true });
470
+ }
471
+ const pastes = (input.pastes ?? []).filter((p) => p.trim().length > 0);
472
+ const content = pastes.length
473
+ ? `${resolved.modelText}${resolved.modelText ? "\n\n" : ""}${pastes.map(wrapPastedText).join("\n\n")}`
474
+ : resolved.modelText;
475
+ const imagePaths = resolved.images.map((i) => i.path);
476
+ const files = attachedFiles(content, session.cwd);
477
+ return { content, images: resolved.images, imagePaths, files, displayText: hideAttachedNames(displayText(content), [...imagePaths, ...files]) };
478
+ }
479
+ /** A user message as the chat shows it: attached file bodies hidden, pastes as their chip. */
480
+ function displayText(content) {
481
+ return collapsePastes(stripAttachments(content));
292
482
  }
293
483
  /**
294
484
  * Continue a session with NO new message — the engine notices what changed on its
@@ -296,6 +486,19 @@ export async function runTurn(session, message, handlers) {
296
486
  * CLI's `reactToBackground`: it calls the exact same `streamRespond` a typed message
297
487
  * would, just without anything new having been said.
298
488
  */
489
+ /**
490
+ * Compact the conversation now, whatever its size: the app's button, the CLI's /compact.
491
+ * It sends the same compaction events an automatic one does inside a turn, then saves.
492
+ */
493
+ export async function compactSession(session, onEvent) {
494
+ await compactNow(session, {
495
+ onActivity: (line, opts) => onEvent({ type: "activity", line, error: opts?.error }),
496
+ onCompaction: (report) => onEvent({ type: "compaction", before: report.before, after: report.after, window: report.window }),
497
+ onCompactionStart: () => onEvent({ type: "compactionStart" }),
498
+ onCompactionEnd: () => onEvent({ type: "compactionEnd" }),
499
+ });
500
+ await saveSession(session);
501
+ }
299
502
  export function continueTurn(session, handlers) {
300
503
  return streamTurn(session, handlers);
301
504
  }
@@ -303,64 +506,254 @@ export function continueTurn(session, handlers) {
303
506
  * boolean out, no Ink involved — so a caller can reuse the exact same rule for when a
304
507
  * finished background command is worth waking the model for. */
305
508
  export { shouldReactToBackground } from "../cli/backgroundWake.js";
509
+ /** A tool call's arguments for display: long strings (a whole file being written) are cut, since a front end
510
+ * that wants the file reads it from disk. */
511
+ const ARG_TEXT_MAX = 2000;
512
+ export function clipArgs(args) {
513
+ const clip = (v) => typeof v === "string" ? (v.length > ARG_TEXT_MAX ? v.slice(0, ARG_TEXT_MAX) : v)
514
+ : Array.isArray(v) ? v.map(clip)
515
+ : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, clip(x)]))
516
+ : v;
517
+ return clip(args ?? {});
518
+ }
519
+ /**
520
+ * One engine event, as the plain TurnEvent a front end draws. Shared by an ordinary turn
521
+ * and a Marathon run so a tool call, a task list or a token reads identically in both.
522
+ */
523
+ export function forwardEngineEvent(e, onEvent) {
524
+ if (e.type === "text") {
525
+ onEvent({ type: "text", delta: e.delta });
526
+ }
527
+ else if (e.type === "reasoning") {
528
+ onEvent({ type: "reasoning", delta: e.delta });
529
+ }
530
+ else if (e.type === "todos") {
531
+ onEvent({ type: "todos", items: e.items });
532
+ }
533
+ else if (e.type === "narration") {
534
+ onEvent({ type: "narration", shown: e.shown });
535
+ }
536
+ else if (e.type === "replyReset") {
537
+ onEvent({ type: "replyReset" });
538
+ }
539
+ else if (e.type === "tool" && e.phase === "start") {
540
+ // The spawn is rendered by the subagentStart block; loading a tool is never shown.
541
+ if (e.name === "spawn_subagent" || e.name === "find_tools")
542
+ return;
543
+ const d = toolDisplay(e.name, e.args);
544
+ if (e.agent) {
545
+ onEvent({ type: "subToolStart", agentId: e.agent, toolId: e.id, name: d.name, arg: d.arg, kind: d.kind });
546
+ }
547
+ else {
548
+ onEvent({ type: "toolStart", id: e.id, name: d.name, arg: d.arg, kind: d.kind, tool: e.name, group: isGroupable(e.name), args: clipArgs(e.args), ...(d.covers ? { covers: d.covers } : {}) });
549
+ }
550
+ }
551
+ else if (e.type === "tool" && e.phase === "end") {
552
+ if (e.name === "spawn_subagent" || e.name === "find_tools")
553
+ return;
554
+ if (e.agent) {
555
+ onEvent({ type: "subToolEnd", agentId: e.agent, toolId: e.id, ok: !e.error, summary: e.summary });
556
+ }
557
+ else {
558
+ onEvent({ type: "toolEnd", id: e.id, ok: !e.error, summary: e.summary, detail: e.detail, detailFull: e.detailFull, detailKind: e.detailKind, quiet: e.quiet, ...(e.images?.length ? { images: e.images } : {}), ...(e.web ? { web: e.web } : {}), ...(e.ui ? { ui: e.ui } : {}) });
559
+ }
560
+ }
561
+ else if (e.type === "tool" && e.phase === "progress") {
562
+ // What a call that takes a while is doing meanwhile (the ui tool waiting for an
563
+ // app to finish building). Latest-wins text, not a log.
564
+ if (!e.agent)
565
+ onEvent({ type: "toolProgress", id: e.id, text: e.text });
566
+ }
567
+ else if (e.type === "subagent" && e.phase === "start") {
568
+ onEvent({ type: "subagentStart", id: e.id, task: e.task, readOnly: e.readOnly });
569
+ }
570
+ else if (e.type === "subagent" && e.phase === "end") {
571
+ onEvent({ type: "subagentEnd", id: e.id, ok: !e.error, summary: e.summary });
572
+ }
573
+ else if (e.type === "usage") {
574
+ onEvent({ type: "usage", promptTokens: e.promptTokens, completionTokens: e.completionTokens, totalTokens: e.totalTokens });
575
+ }
576
+ }
577
+ /**
578
+ * A key refused for credit, a rate limit or rejection: when the user allowed it, move to
579
+ * their next key so the caller can carry on from where the turn stopped. A refusal happens
580
+ * on the status line, before any reply arrived, so nothing is half-written to resume over.
581
+ * True when it switched; false leaves the error for the caller to report.
582
+ */
583
+ function tryFailover(session, error, signal, triedKeys, onEvent) {
584
+ const provider = providerOf(session.modelConfig.model);
585
+ const status = statusOf(error);
586
+ if (status === null || signal?.aborted || !accessRefusal(error, provider.label, false))
587
+ return false;
588
+ const moved = failoverKey(manifestForModel(session.modelConfig.model).apiKeyEnv, status, triedKeys);
589
+ if (!moved)
590
+ return false;
591
+ const why = moved.reason === "no-credit" ? "is out of credit" : moved.reason === "rate-limited" ? "hit its rate limit" : "was rejected";
592
+ onEvent({ type: "activity", line: `${provider.label} key ${moved.from} ${why}, switched to ${moved.to}` });
593
+ return true;
594
+ }
306
595
  async function streamTurn(session, handlers) {
307
596
  const { onEvent, steer, signal } = handlers;
597
+ // Keys already refused in THIS turn, so switching can't go round in a circle.
598
+ const triedKeys = new Set();
599
+ for (;;) {
600
+ try {
601
+ await respond(session, {
602
+ steer,
603
+ signal,
604
+ onActivity: (line, opts) => onEvent({ type: "activity", line, error: opts?.error }),
605
+ onCompaction: (report) => onEvent({ type: "compaction", before: report.before, after: report.after, window: report.window }),
606
+ onCompactionStart: () => onEvent({ type: "compactionStart" }),
607
+ onCompactionEnd: () => onEvent({ type: "compactionEnd" }),
608
+ onEvent: (e) => forwardEngineEvent(e, onEvent),
609
+ persist: () => saveSession(session),
610
+ });
611
+ noteKeyWorked(manifestForModel(session.modelConfig.model).apiKeyEnv);
612
+ onEvent({ type: "done" });
613
+ await saveSession(session);
614
+ return;
615
+ }
616
+ catch (error) {
617
+ if (tryFailover(session, error, signal, triedKeys, onEvent))
618
+ continue;
619
+ reportFailure(session, error, onEvent);
620
+ onEvent({ type: "done" });
621
+ await saveSession(session);
622
+ return;
623
+ }
624
+ }
625
+ }
626
+ /** A turn that ended in an error, as a notice (a refusal or an outage the user can act on)
627
+ * or a plain error line. */
628
+ function reportFailure(session, error, onEvent) {
629
+ const refusal = accessRefusal(error, providerOf(session.modelConfig.model).label, false) ??
630
+ providerOutage(error, providerOf(session.modelConfig.model).label, modelLabel(session.modelConfig.model));
631
+ if (refusal)
632
+ onEvent({ type: "notice", title: refusal.title, body: refusal.body });
633
+ else
634
+ onEvent({ type: "error", text: errText(error) });
635
+ }
636
+ /**
637
+ * Run a Marathon for a front end: the same events an ordinary turn streams, plus the
638
+ * run's own progress and the live task list, with the same key failover and error
639
+ * reporting. `run` is the start/resume call; `override` swaps the engine calls for a fake
640
+ * in a test.
641
+ */
642
+ async function driveMarathon(session, handlers, run, override = {}) {
643
+ const { onEvent, steer, signal } = handlers;
644
+ const triedKeys = new Set();
645
+ const deps = {
646
+ // Every turn of the run gets the failover an ordinary turn gets.
647
+ respond: async (s, options) => {
648
+ for (;;) {
649
+ try {
650
+ return await respond(s, options);
651
+ }
652
+ catch (error) {
653
+ if (tryFailover(s, error, signal, triedKeys, onEvent))
654
+ continue;
655
+ throw error;
656
+ }
657
+ }
658
+ },
659
+ verify: verifyGoal,
660
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
661
+ ...override,
662
+ };
663
+ let state = null;
308
664
  try {
309
- await respond(session, {
310
- steer,
665
+ state = await run({
311
666
  signal,
667
+ ...(steer ? { steer } : {}),
668
+ ...(handlers.limits ? { limits: handlers.limits } : {}),
312
669
  onActivity: (line, opts) => onEvent({ type: "activity", line, error: opts?.error }),
313
670
  onCompaction: (report) => onEvent({ type: "compaction", before: report.before, after: report.after, window: report.window }),
314
- onEvent: (e) => {
315
- if (e.type === "text") {
316
- onEvent({ type: "text", delta: e.delta });
317
- }
318
- else if (e.type === "tool" && e.phase === "start") {
319
- if (e.name === "spawn_subagent")
320
- return; // rendered by the subagentStart block, not a raw row
321
- const d = toolDisplay(e.name, e.args);
322
- if (e.agent) {
323
- onEvent({ type: "subToolStart", agentId: e.agent, toolId: e.id, name: d.name, arg: d.arg, kind: d.kind });
324
- }
325
- else {
326
- onEvent({ type: "toolStart", id: e.id, name: d.name, arg: d.arg, kind: d.kind, group: isGroupable(e.name), ...(d.covers ? { covers: d.covers } : {}) });
327
- }
328
- }
329
- else if (e.type === "tool" && e.phase === "end") {
330
- if (e.name === "spawn_subagent")
331
- return;
332
- if (e.agent) {
333
- onEvent({ type: "subToolEnd", agentId: e.agent, toolId: e.id, ok: !e.error, summary: e.summary });
334
- }
335
- else {
336
- onEvent({ type: "toolEnd", id: e.id, ok: !e.error, summary: e.summary, detail: e.detail, detailFull: e.detailFull, detailKind: e.detailKind, quiet: e.quiet });
337
- }
338
- }
339
- else if (e.type === "subagent" && e.phase === "start") {
340
- onEvent({ type: "subagentStart", id: e.id, task: e.task, readOnly: e.readOnly });
341
- }
342
- else if (e.type === "subagent" && e.phase === "end") {
343
- onEvent({ type: "subagentEnd", id: e.id, ok: !e.error, summary: e.summary });
344
- }
345
- else if (e.type === "usage") {
346
- onEvent({ type: "usage", promptTokens: e.promptTokens, completionTokens: e.completionTokens, totalTokens: e.totalTokens });
347
- }
348
- },
671
+ onCompactionStart: () => onEvent({ type: "compactionStart" }),
672
+ onCompactionEnd: () => onEvent({ type: "compactionEnd" }),
673
+ onEvent: (e) => forwardEngineEvent(e, onEvent),
674
+ onMarathon: (event) => onEvent({ type: "marathon", event, text: describeMarathonEvent(event) }),
349
675
  persist: () => saveSession(session),
350
- });
351
- onEvent({ type: "done" });
676
+ }, deps);
677
+ noteKeyWorked(manifestForModel(session.modelConfig.model).apiKeyEnv);
352
678
  }
353
679
  catch (error) {
354
- const refusal = accessRefusal(error, providerOf(session.modelConfig.model).label, false) ??
355
- providerOutage(error, providerOf(session.modelConfig.model).label, modelLabel(session.modelConfig.model));
356
- if (refusal)
357
- onEvent({ type: "notice", title: refusal.title, body: refusal.body });
358
- else
359
- onEvent({ type: "error", text: errText(error) });
360
- onEvent({ type: "done" });
680
+ // The run stays "running" on the session, so it can be resumed once the cause is fixed.
681
+ reportFailure(session, error, onEvent);
682
+ }
683
+ onEvent({ type: "done" });
684
+ await saveSession(session);
685
+ return state;
686
+ }
687
+ /**
688
+ * Start a Marathon on `goal`: the user armed it and sent their goal. Replaces any
689
+ * Marathon the session already holds. Resolves when the run reaches an outcome or is
690
+ * stopped (the same signal that stops a turn); a stopped run stays resumable.
691
+ */
692
+ export async function startMarathonRun(session, goal, handlers, override) {
693
+ // Same preparation as the first message of any turn: the project's notes re-read, and
694
+ // attached images resolved (and rejected loudly) the way `runTurn` does it.
695
+ const message = typeof goal === "string" ? { content: goal } : goal;
696
+ await reloadProjectMemory(session).catch(() => { });
697
+ const goalImages = [
698
+ ...(("images" in message && message.images) || []),
699
+ ...(await resolveImages(("imagePaths" in message && message.imagePaths) || [], handlers.onEvent)),
700
+ ];
701
+ return driveMarathon(session, handlers, (options, deps) => startMarathon(session, message.content, { ...options, ...(goalImages.length ? { goalImages } : {}) }, deps), override);
702
+ }
703
+ /** Carry on a Marathon that was stopped, or that a restart interrupted. Resolves null
704
+ * when there is nothing to resume. */
705
+ export function resumeMarathonRun(session, handlers, override) {
706
+ return driveMarathon(session, handlers, (options, deps) => resumeMarathon(session, options, deps), override);
707
+ }
708
+ /** Close a Marathon for good: dropped from the session AND from its saved file, so
709
+ * reopening the session later does not bring a dismissed run back. */
710
+ export async function dismissMarathon(session) {
711
+ clearMarathon(session);
712
+ await saveSession(session);
713
+ }
714
+ /** The session's Marathon, if it has one — running, stopped, or finished. */
715
+ export function marathonOf(session) {
716
+ return session.marathon ?? null;
717
+ }
718
+ /**
719
+ * Start a command the USER asked for (the app's Run button) as a background shell, through
720
+ * the same run_command tool the agent uses, so it lands in the same shell list, the agent
721
+ * can read its output, and stopping it is the same kill. The click is the approval, so no
722
+ * gate is asked. `notify: "never"` because the user starting their own app is not an event
723
+ * to wake the model for. Returns the shell id, or an error message.
724
+ */
725
+ export async function startUserCommand(session, command, cwd) {
726
+ // A fresh context: the session's may still carry the last turn's (aborted) signal.
727
+ // `cwd` is for an app that lives in a subfolder of the project.
728
+ const ctx = { ...session.toolContext, abortSignal: undefined, cwd: cwd ?? session.toolContext.cwd };
729
+ // hidden: false because the user pressed Run to see their app, whatever its script says.
730
+ const result = await runCommand.execute({ command, run_in_background: true, notify: "never", hidden: false }, ctx);
731
+ const id = /shell #(\d+)/.exec(`${result.summary ?? ""} ${result.output}`)?.[1];
732
+ if (result.isError || !id)
733
+ return { error: result.output || "Could not start the command." };
734
+ return { id: Number(id) };
735
+ }
736
+ /** The project's MINDWEAVE.md (in the project root, the notebook the agent keeps) and
737
+ * what it says. A missing file is empty, not an error. */
738
+ export async function readProjectNotes(cwd) {
739
+ const path = join(cwd, NOTES_FILE);
740
+ try {
741
+ const [text, info] = await Promise.all([readFile(path, "utf8"), stat(path)]);
742
+ return { path, text, exists: true, modifiedAt: info.mtimeMs };
361
743
  }
362
- finally {
363
- await saveSession(session);
744
+ catch {
745
+ return { path, text: "", exists: false, modifiedAt: null };
364
746
  }
365
747
  }
748
+ /**
749
+ * Replace the project's MINDWEAVE.md, then re-read it into the live session so the NEXT
750
+ * message already follows it. That costs one prompt-cache rebuild, which is what an edit
751
+ * the user made on purpose is worth; the model's own mid-session edits keep the cheaper
752
+ * deferred path.
753
+ */
754
+ export async function saveProjectNotes(cwd, text, session) {
755
+ await writeFileAtomic(join(cwd, NOTES_FILE), text);
756
+ if (session)
757
+ await reloadProjectMemory(session, { force: true });
758
+ }
366
759
  //# sourceMappingURL=turnRunner.js.map