mindweave 2.4.9 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +145 -144
  2. package/dist/cli/App.js +47 -8
  3. package/dist/cli/App.js.map +1 -1
  4. package/dist/cli/commandArgs.js +7 -1
  5. package/dist/cli/commandArgs.js.map +1 -1
  6. package/dist/cli/commands.js +1 -0
  7. package/dist/cli/commands.js.map +1 -1
  8. package/dist/cli/components/ApprovalBox.js +10 -1
  9. package/dist/cli/components/ApprovalBox.js.map +1 -1
  10. package/dist/cli/components/McpMinitabs.js +1 -1
  11. package/dist/cli/components/Picker.js +1 -1
  12. package/dist/cli/components/Picker.js.map +1 -1
  13. package/dist/cli/components/ToolLine.js +1 -1
  14. package/dist/cli/components/ToolLine.js.map +1 -1
  15. package/dist/cli/feedback.js +223 -0
  16. package/dist/cli/feedback.js.map +1 -0
  17. package/dist/cli/framebuffer/writer.js +1 -1
  18. package/dist/cli/toolDisplay.js +4 -3
  19. package/dist/cli/toolDisplay.js.map +1 -1
  20. package/dist/core/turnRunner.js +366 -0
  21. package/dist/core/turnRunner.js.map +1 -0
  22. package/dist/drivers/anthropic/client.js +77 -21
  23. package/dist/drivers/anthropic/client.js.map +1 -1
  24. package/dist/drivers/anthropic/manifest.js +28 -11
  25. package/dist/drivers/anthropic/manifest.js.map +1 -1
  26. package/dist/drivers/cerebras/manifest.js +7 -1
  27. package/dist/drivers/cerebras/manifest.js.map +1 -1
  28. package/dist/drivers/deepseek/manifest.js +12 -31
  29. package/dist/drivers/deepseek/manifest.js.map +1 -1
  30. package/dist/drivers/glm/manifest.js +18 -5
  31. package/dist/drivers/glm/manifest.js.map +1 -1
  32. package/dist/drivers/kimi/manifest.js +13 -10
  33. package/dist/drivers/kimi/manifest.js.map +1 -1
  34. package/dist/drivers/meta/manifest.js +21 -16
  35. package/dist/drivers/meta/manifest.js.map +1 -1
  36. package/dist/drivers/minimax/manifest.js +15 -12
  37. package/dist/drivers/minimax/manifest.js.map +1 -1
  38. package/dist/drivers/openai/manifest.js +24 -7
  39. package/dist/drivers/openai/manifest.js.map +1 -1
  40. package/dist/drivers/qwen/manifest.js +19 -6
  41. package/dist/drivers/qwen/manifest.js.map +1 -1
  42. package/dist/drivers/xai/client.js +12 -10
  43. package/dist/drivers/xai/client.js.map +1 -1
  44. package/dist/drivers/xai/manifest.js +62 -25
  45. package/dist/drivers/xai/manifest.js.map +1 -1
  46. package/dist/dynamo/engine.js +179 -99
  47. package/dist/dynamo/engine.js.map +1 -1
  48. package/dist/governor/skills.js +1 -1
  49. package/dist/governor/write.js +61 -0
  50. package/dist/governor/write.js.map +1 -1
  51. package/dist/mcp/configWrite.js +1 -1
  52. package/dist/memory/compaction.js +10 -2
  53. package/dist/memory/compaction.js.map +1 -1
  54. package/dist/memory/store.js +8 -1
  55. package/dist/memory/store.js.map +1 -1
  56. package/dist/tools/backgroundShells.js +4 -3
  57. package/dist/tools/backgroundShells.js.map +1 -1
  58. package/dist/tools/deferredNative.js +13 -4
  59. package/dist/tools/deferredNative.js.map +1 -1
  60. package/dist/tools/detail.js +9 -6
  61. package/dist/tools/detail.js.map +1 -1
  62. package/dist/tools/edit.js +2 -1
  63. package/dist/tools/edit.js.map +1 -1
  64. package/dist/tools/governorTools.js +113 -9
  65. package/dist/tools/governorTools.js.map +1 -1
  66. package/dist/tools/mcpAdd.js +87 -4
  67. package/dist/tools/mcpAdd.js.map +1 -1
  68. package/dist/tools/mcpSearch.js +5 -4
  69. package/dist/tools/mcpSearch.js.map +1 -1
  70. package/dist/tools/mindweaveStatus.js +113 -0
  71. package/dist/tools/mindweaveStatus.js.map +1 -0
  72. package/dist/tools/nativeStderr.js +27 -0
  73. package/dist/tools/nativeStderr.js.map +1 -0
  74. package/dist/tools/registry.js +6 -4
  75. package/dist/tools/registry.js.map +1 -1
  76. package/dist/tools/replaceSymbol.js +2 -1
  77. package/dist/tools/replaceSymbol.js.map +1 -1
  78. package/dist/tools/runCommand.js +59 -21
  79. package/dist/tools/runCommand.js.map +1 -1
  80. package/dist/tools/screenshot.js +17 -4
  81. package/dist/tools/screenshot.js.map +1 -1
  82. package/dist/tools/todo.js +5 -5
  83. package/dist/tools/todo.js.map +1 -1
  84. package/dist/tools/writeFile.js +2 -1
  85. package/dist/tools/writeFile.js.map +1 -1
  86. package/package.json +7 -3
@@ -27,7 +27,7 @@ import { basePrompt } from "./prompt.js";
27
27
  import { basename } from "node:path";
28
28
  import { randomUUID } from "node:crypto";
29
29
  import { promises as fsp } from "node:fs";
30
- import { resolvePath, rootLabel, rootsOf } from "../tools/paths.js";
30
+ import { relativize, resolvePath, rootLabel, rootsOf } from "../tools/paths.js";
31
31
  import { renderRules, renderSkillCatalog, reloadGovernance, governanceStamp, rescope } from "../governor/index.js";
32
32
  import { forkSession, reloadProjectMemory } from "../memory/session.js";
33
33
  import { selectActiveFiles } from "../memory/workingSet.js";
@@ -58,11 +58,11 @@ const MAX_COMPACT_FAILURES = 3;
58
58
  export function staticSystemPrompt(projectContext, projectMemory, memoryDir, memoryIndex, governance, workspace, priorSessions = 0) {
59
59
  let prompt = basePrompt(commandShellLabel());
60
60
  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}
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}
66
66
  </workspace>`;
67
67
  }
68
68
  // NOTE: the user's standing rules are deliberately NOT rendered here. They live
@@ -73,41 +73,41 @@ ${workspace}
73
73
  // skills are a reference catalog), so they alone get the salience boost. Keeping
74
74
  // them out of the prefix also stops a mid-session `remember_rule` from busting it.
75
75
  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}
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}
81
81
  </forbidden>`;
82
82
  }
83
83
  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}
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}
89
89
  </forbidden_commands>`;
90
90
  }
91
91
  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}
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}
97
97
  </available_skills>`;
98
98
  }
99
99
  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):
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):
103
103
  ${projectContext}`;
104
104
  }
105
105
  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}
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}
111
111
  </project_memory>`;
112
112
  }
113
113
  // Its own past work in this project. The COUNT goes in the prompt (so the model
@@ -117,16 +117,16 @@ ${projectMemory}
117
117
  // and a question about past work gets a real answer instead of a deflection.
118
118
  if (priorSessions > 0) {
119
119
  const s = priorSessions === 1 ? "" : "s";
120
- prompt += `
121
-
120
+ prompt += `
121
+
122
122
  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
123
  }
124
124
  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)"}
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)"}
130
130
  </memory_index>`;
131
131
  }
132
132
  // The deferred pool's index. Roughly forty tokens standing in for several hundred of
@@ -134,8 +134,8 @@ ${memoryIndex || "(empty — nothing has been saved to memory yet)"}
134
134
  // missing feature, and the model routes around a capability it actually has.
135
135
  const deferred = deferredToolsIndex();
136
136
  if (deferred) {
137
- prompt += `
138
-
137
+ prompt += `
138
+
139
139
  ${deferred}`;
140
140
  }
141
141
  return prompt;
@@ -156,8 +156,17 @@ ${deferred}`;
156
156
  const ACTIVE_FILES_FOR_NOTES = 20;
157
157
  export function volatileContext(rules, planMode, sessionMemory, approvedPlan = "",
158
158
  /** Notes for the folders being worked in right now (see memory/projectNotes.ts). */
159
- directoryNotes = []) {
159
+ directoryNotes = [],
160
+ /** Where the previous turn left the shell, when this turn started back at the root. */
161
+ cwdResetFrom = "") {
160
162
  const parts = [];
163
+ // Each turn starts at the project root, and a model that `cd`-ed into a subfolder last
164
+ // turn does not know that unless it is told. It was not, and a real session ran
165
+ // `cargo run` from the root expecting the subfolder: "could not find Cargo.toml".
166
+ if (cwdResetFrom) {
167
+ parts.push(`Commands run from the project root. The previous turn had moved into ${cwdResetFrom}, but each ` +
168
+ `turn starts back at the root: cd there again, or use paths from the root.`);
169
+ }
161
170
  // Standing rules FIRST in the volatile tail. They're rebuilt every turn here (not
162
171
  // in the cached prefix), so a long conversation can never bury them — and they sit
163
172
  // at the top of the freshest context the model reads before it acts. Binding by
@@ -442,6 +451,12 @@ function workspaceText(session) {
442
451
  function modelSeesImages(session) {
443
452
  return manifestForModel(session.modelConfig.model).acceptsImages?.(session.modelConfig.model) ?? false;
444
453
  }
454
+ /** "JPG and PNG" from `["image/png", "image/jpeg"]` — the names a user knows the files by. */
455
+ export function imageTypeNames(types) {
456
+ const names = types.map((t) => ({ "image/jpeg": "JPG", "image/png": "PNG", "image/gif": "GIF", "image/webp": "WebP" })[t] ?? t);
457
+ const sorted = [...new Set(names)].sort();
458
+ return sorted.length <= 1 ? (sorted[0] ?? "") : `${sorted.slice(0, -1).join(", ")} and ${sorted[sorted.length - 1]}`;
459
+ }
445
460
  async function loadImagePayloads(session) {
446
461
  // Nothing to load for a model that cannot look at one. This is the /provider switch
447
462
  // case: a picture attached while a vision model was running stays in the transcript,
@@ -471,6 +486,11 @@ function buildRequest(session, tools, imagePayloads = new Map(),
471
486
  /** Notes for the folders in play, resolved by the caller (it has to read disk). */
472
487
  directoryNotes = []) {
473
488
  const canSeeImages = modelSeesImages(session);
489
+ // The formats it takes, when narrower than everything core attaches. A manifest fact,
490
+ // like vision itself; absent means every type.
491
+ const imageTypes = canSeeImages
492
+ ? manifestForModel(session.modelConfig.model).imageTypes?.(session.modelConfig.model)
493
+ : undefined;
474
494
  const messages = [];
475
495
  for (const e of session.transcript) {
476
496
  if (e.role === "user" || e.role === "summary") {
@@ -487,6 +507,7 @@ directoryNotes = []) {
487
507
  const images = [];
488
508
  const missing = [];
489
509
  const unseen = [];
510
+ const wrongType = [];
490
511
  for (const ref of refs) {
491
512
  // Told, never silently dropped. A message that mentions a screenshot and
492
513
  // carries nothing reads to the model as a picture it failed to notice; the
@@ -495,6 +516,12 @@ directoryNotes = []) {
495
516
  unseen.push(basename(ref.path));
496
517
  continue;
497
518
  }
519
+ // A format the provider would reject: held back the same way, so the model
520
+ // can tell the user which formats work instead of the whole request failing.
521
+ if (imageTypes && !imageTypes.includes(ref.mediaType)) {
522
+ wrongType.push(basename(ref.path));
523
+ continue;
524
+ }
498
525
  const data = imagePayloads.get(ref.path);
499
526
  if (data)
500
527
  images.push({ path: ref.path, mediaType: ref.mediaType, data });
@@ -505,10 +532,17 @@ directoryNotes = []) {
505
532
  ...(unseen.length > 0
506
533
  ? [`${unseen.join(", ")} was attached, but the model now running cannot see images`]
507
534
  : []),
535
+ ...(wrongType.length > 0 && imageTypes
536
+ ? [
537
+ `${wrongType.join(", ")} was attached but not sent: the model now running ` +
538
+ `accepts only ${imageTypeNames(imageTypes)} images, so tell the user to ` +
539
+ `convert it or switch models`,
540
+ ]
541
+ : []),
508
542
  ...(missing.length > 0 ? [`${missing.join(", ")} could not be read from disk`] : []),
509
543
  ];
510
- const content = notes.length > 0 ? `${said}
511
-
544
+ const content = notes.length > 0 ? `${said}
545
+
512
546
  [${notes.join("; ")}]` : said;
513
547
  messages.push({ role: "user", content, ...(images.length > 0 ? { images } : {}) });
514
548
  continue;
@@ -543,7 +577,12 @@ directoryNotes = []) {
543
577
  approvedAt: session.toolContext.activePlanApprovedAt ?? "",
544
578
  mode: "lightning",
545
579
  })
546
- : "", directoryNotes),
580
+ : "", directoryNotes,
581
+ // Only while the turn is still sitting where the reset put it. Once a command moves,
582
+ // the command's own result says where it is now.
583
+ session.toolContext.cwdResetFrom && session.toolContext.cwd === session.cwd
584
+ ? relativize(session.toolContext, session.toolContext.cwdResetFrom)
585
+ : ""),
547
586
  tools,
548
587
  model: session.modelConfig,
549
588
  };
@@ -558,55 +597,83 @@ async function backgroundEventNotes(session) {
558
597
  if (!mgr)
559
598
  return [];
560
599
  const events = await mgr.drainEvents();
561
- return events.map(({ info, kind, tail, wake }) => {
562
- // It came up. This is the only positive event a server ever produces, and it is
563
- // what lets the model actually deliver the "I'll tell you when it's running" it
564
- // was told to say. Nothing has gone wrong, so there is nothing to fix.
565
- if (kind === "ready") {
566
- return (`[Background shell #${info.id} (\`${info.command}\`) is up and running.]\n` +
567
- `Recent output:\n${tail || "(no output)"}\n\n` +
568
- `Tell the user in one short line that it's running. Nothing is wrong — do not investigate, ` +
569
- `do not restart it, and do not change any files because of this.`);
570
- }
571
- // The watchdog thinks this running shell is stuck. It has produced nothing for a
572
- // while — either blocked on a prompt it will never answer, or silently wedged on a
573
- // command that should have kept working. The point is to stop it sitting invisible
574
- // until the timeout, and to hand the model the two moves that resolve it.
575
- if (kind === "stalled") {
576
- const why = info.stallReason === "prompt"
577
- ? `It looks like it is waiting for interactive input (its last line reads as a prompt). ` +
578
- `Kill it with kill_shell #${info.id} and re-run non-interactively — pipe the answer in ` +
579
- `(e.g. \`echo y | …\`) or add a non-interactive flag like \`-y\`/\`--yes\`.`
580
- : `It has produced no output for a long time and may be wedged. Read it with shells #${info.id} ` +
581
- `to judge, then either keep waiting if it is genuinely mid-work, or kill it with ` +
582
- `kill_shell #${info.id} and look into why it hangs.`;
583
- return (`[Background shell #${info.id} (\`${info.command}\`) appears to be stuck.]\n` +
584
- `Recent output:\n${tail || "(no output)"}\n\n${why}`);
585
- }
586
- const status = info.status === "killed"
587
- ? info.stoppedBy === "user"
588
- ? "was stopped by the user"
589
- : "was killed"
590
- : `finished with exit code ${info.exitCode}`;
591
- // An ending that is NOT worth interrupting for still arrives, so the model knows the
592
- // thing is down and can answer about it. It is explicitly not a task: this is the
593
- // path a user closing their own app takes, and treating it as news is what made the
594
- // agent reopen it.
595
- if (!wake) {
596
- return (`[Background shell #${info.id} (\`${info.command}\`) ${status}. It had already started up, so ` +
597
- `this is the user stopping their own app, not a failure.]\n` +
598
- `This is background information only. Do NOT mention it unless it is relevant, do NOT restart ` +
599
- `it, and do NOT change any files because of it. If the user later asks about this app, you now ` +
600
- `know it is stopped.`);
601
- }
602
- // For a server, only a failure to come up reaches here: a normal stop does not wake.
603
- const guidance = info.notify === "on_failure"
604
- ? "This is a server or app that never came up, so the user never saw it running. Tell them what happened and offer to fix it — but do not restart it repeatedly on your own."
605
- : "If it failed, tell the user briefly what went wrong and propose a fix — don't change files unless they agree.";
606
- return (`[Background shell #${info.id} (\`${info.command}\`) ${status}.]\n` +
600
+ return events.map(backgroundEventNote).filter((note) => note !== null);
601
+ }
602
+ /**
603
+ * The note for one background-shell event, or null when there is nothing to say (pure).
604
+ *
605
+ * An ending the AGENT caused says nothing: `kill_shell` already told it the shell
606
+ * stopped. The note that used to follow declared "this is the user stopping their own
607
+ * app", which blamed the user for the agent's own restart and gave the model a second,
608
+ * contradictory account of the same event.
609
+ */
610
+ export function backgroundEventNote({ info, kind, tail, wake, }) {
611
+ // It came up. This is the only positive event a server ever produces, and it is
612
+ // what lets the model actually deliver the "I'll tell you when it's running" it
613
+ // was told to say. Nothing has gone wrong, so there is nothing to fix.
614
+ if (kind === "ready") {
615
+ return (`[Background shell #${info.id} (\`${info.command}\`) is up and running.]\n` +
607
616
  `Recent output:\n${tail || "(no output)"}\n\n` +
608
- guidance);
609
- });
617
+ `Tell the user in one short line that it's running. Nothing is wrong — do not investigate, ` +
618
+ `do not restart it, and do not change any files because of this. Running only means the ` +
619
+ `process started: do not describe what it shows or say a change is visible unless you ` +
620
+ `have actually looked.`);
621
+ }
622
+ // The watchdog thinks this running shell is stuck. It has produced nothing for a
623
+ // while — either blocked on a prompt it will never answer, or silently wedged on a
624
+ // command that should have kept working. The point is to stop it sitting invisible
625
+ // until the timeout, and to hand the model the two moves that resolve it.
626
+ if (kind === "stalled") {
627
+ const why = info.stallReason === "prompt"
628
+ ? `It looks like it is waiting for interactive input (its last line reads as a prompt). ` +
629
+ `Kill it with kill_shell #${info.id} and re-run non-interactively — pipe the answer in ` +
630
+ `(e.g. \`echo y | …\`) or add a non-interactive flag like \`-y\`/\`--yes\`.`
631
+ : `It has produced no output for a long time and may be wedged. Read it with shells #${info.id} ` +
632
+ `to judge, then either keep waiting if it is genuinely mid-work, or kill it with ` +
633
+ `kill_shell #${info.id} and look into why it hangs.`;
634
+ return (`[Background shell #${info.id} (\`${info.command}\`) appears to be stuck.]\n` +
635
+ `Recent output:\n${tail || "(no output)"}\n\n${why}`);
636
+ }
637
+ const status = info.status === "killed"
638
+ ? info.stoppedBy === "user"
639
+ ? "was stopped by the user"
640
+ : "was killed"
641
+ : `finished with exit code ${info.exitCode}`;
642
+ // An ending that is NOT worth interrupting for still arrives, so the model knows the
643
+ // thing is down and can answer about it. It is explicitly not a task: this is the
644
+ // path a user closing their own app takes, and treating it as news is what made the
645
+ // agent reopen it.
646
+ if (info.status === "killed" && info.stoppedBy === "agent")
647
+ return null;
648
+ // Mindweave stopped it itself, because its output ran past the size cap. That is a
649
+ // runaway, not someone closing an app, and the model is the one who can explain it.
650
+ if (info.status === "killed" && info.stoppedBy === "system") {
651
+ return (`[Background shell #${info.id} (\`${info.command}\`) was stopped by Mindweave because its output ` +
652
+ `passed the size limit.]\n` +
653
+ `Recent output:\n${tail || "(no output)"}\n\n` +
654
+ `Something in it was writing without end. Tell the user, and look at the output above before ` +
655
+ `running it again.`);
656
+ }
657
+ if (!wake) {
658
+ // Only a stop the user made through the app is known to be theirs. An app that exited
659
+ // on its own after coming up was most likely closed by them, which is worth saying as
660
+ // the likely reading rather than as a fact.
661
+ const who = info.status === "killed" && info.stoppedBy === "user"
662
+ ? "the user stopping their own app"
663
+ : "most likely the user closing their own app";
664
+ return (`[Background shell #${info.id} (\`${info.command}\`) ${status}. It had already started up, so ` +
665
+ `this is ${who}, not a failure.]\n` +
666
+ `This is background information only. Do NOT mention it unless it is relevant, do NOT restart ` +
667
+ `it, and do NOT change any files because of it. If the user later asks about this app, you now ` +
668
+ `know it is stopped.`);
669
+ }
670
+ // For a server, only a failure to come up reaches here: a normal stop does not wake.
671
+ const guidance = info.notify === "on_failure"
672
+ ? "This is a server or app that never came up, so the user never saw it running. If you started it to check work you are still doing, getting it running is part of that work: find out why it failed and fix it. Otherwise tell them what happened and offer to fix it. Either way, do not restart it again without changing something first."
673
+ : "If it failed, tell the user briefly what went wrong and propose a fix — don't change files unless they agree.";
674
+ return (`[Background shell #${info.id} (\`${info.command}\`) ${status}.]\n` +
675
+ `Recent output:\n${tail || "(no output)"}\n\n` +
676
+ guidance);
610
677
  }
611
678
  /**
612
679
  * Produce Mindweave's next reply for the latest user message already on
@@ -696,13 +763,13 @@ export async function respond(session, options = {}) {
696
763
  function implementFromScratch(priorPath, plan) {
697
764
  const path = priorPath;
698
765
  const where = path
699
- ? `
700
-
766
+ ? `
767
+
701
768
  If you need something exact from the planning that produced this — a snippet, an ` +
702
769
  `error message, a path — the full conversation is at: ${path}`
703
770
  : "";
704
- return `Implement the following plan:
705
-
771
+ return `Implement the following plan:
772
+
706
773
  ${plan}${where}`;
707
774
  }
708
775
  /**
@@ -803,12 +870,16 @@ async function respondTurn(session, options = {}) {
803
870
  // (its lifecycle + tagged tool calls) up this same stream instead of running dark.
804
871
  session.toolContext.emitEvent = options.onEvent;
805
872
  session.toolContext.abortSignal = options.signal;
873
+ // So a tool can answer "which model are you running" instead of guessing.
874
+ session.toolContext.modelConfig = session.modelConfig;
806
875
  // WORKING-DIRECTORY RESET. Each turn starts at the project root — the working
807
876
  // directory is already set to the correct project directory automatically. Within a
808
877
  // turn cd still persists (so a multi-step command sequence works), but it never
809
878
  // carries a stale `cd` into the next
810
879
  // turn — the bug where `cd src-tauri` run in two turns became `…/src-tauri/src-tauri`.
811
880
  // The primary root (session.cwd) is fixed; only toolContext.cwd moves.
881
+ const leftIn = session.toolContext.cwd;
882
+ session.toolContext.cwdResetFrom = leftIn && leftIn !== session.cwd ? leftIn : undefined;
812
883
  session.toolContext.cwd = session.cwd;
813
884
  // TASK-BOUNDARY SWEEP. If the previous turn finished a task (a todo list completed)
814
885
  // and this new message opens a DIFFERENT one (not a "continue"), close the finished
@@ -836,6 +907,10 @@ async function respondTurn(session, options = {}) {
836
907
  const limits = taskLimits();
837
908
  const startedAt = Date.now();
838
909
  const usages = [];
910
+ // When each of those calls returned. Recorded as it happens: stamping them when the
911
+ // turn is saved gave every call in a turn the same time, so the call log could not say
912
+ // how a long turn's time was spent (one real session: 145 calls, 4 distinct times).
913
+ const usageTimes = [];
839
914
  // Verification-gate bookkeeping for this turn: did the model change any file,
840
915
  // did it ever run a check, and have we already nudged once (one-shot).
841
916
  let mutatedThisTurn = false;
@@ -898,7 +973,7 @@ async function respondTurn(session, options = {}) {
898
973
  // of them — those are different problems with different fixes, and the totals look
899
974
  // identical for all of them. Six numbers per call, capped, so a long session cannot
900
975
  // grow the meta file without bound.
901
- session.callLog = [...(session.callLog ?? []), ...usages.map((u) => toCallRecord(u, session.modelConfig.model))].slice(-CALL_LOG_LIMIT);
976
+ session.callLog = [...(session.callLog ?? []), ...usages.map((u, i) => toCallRecord(u, session.modelConfig.model, usageTimes[i]))].slice(-CALL_LOG_LIMIT);
902
977
  };
903
978
  try {
904
979
  const reply = await runTurn();
@@ -1070,6 +1145,7 @@ async function respondTurn(session, options = {}) {
1070
1145
  emitUsage(result, options);
1071
1146
  if (result.usage) {
1072
1147
  usages.push(result.usage);
1148
+ usageTimes.push(Date.now());
1073
1149
  writeCacheLog(cacheCallLine({
1074
1150
  call: usages.length,
1075
1151
  gapMs: sinceLastCall,
@@ -1337,6 +1413,7 @@ async function respondTurn(session, options = {}) {
1337
1413
  summary: result.summary,
1338
1414
  isError: result.isError,
1339
1415
  detail: result.detail,
1416
+ detailFull: result.detailFull,
1340
1417
  detailKind: result.detailKind,
1341
1418
  quiet: result.quiet,
1342
1419
  fullContentOf: result.fullContentOf,
@@ -1360,6 +1437,7 @@ async function respondTurn(session, options = {}) {
1360
1437
  summary: r.summary ?? r.call.name,
1361
1438
  error: r.isError ?? false,
1362
1439
  detail: r.detail,
1440
+ ...("detailFull" in r && r.detailFull ? { detailFull: r.detailFull } : {}),
1363
1441
  ...(r.detailKind ? { detailKind: r.detailKind } : {}),
1364
1442
  ...(r.quiet ? { quiet: true } : {}),
1365
1443
  ...(r.displayKind ? { displayKind: r.displayKind } : {}),
@@ -1493,8 +1571,10 @@ async function respondTurn(session, options = {}) {
1493
1571
  session.transcript.push({
1494
1572
  role: "user",
1495
1573
  content: canSee
1496
- ? `Here ${shots.length === 1 ? "is the image" : "are the images"} just captured (${names}).`
1497
- : `${names} was captured and saved, but this model cannot see images, so you are ` +
1574
+ ? // Not "just captured": view_image opens files that already existed (the user's own
1575
+ // screenshots), and saying they were captured tells the model it took them.
1576
+ `Here ${shots.length === 1 ? "is the image" : "are the images"} from the tool call above (${names}).`
1577
+ : `${names} is ready, but this model cannot see images, so you are ` +
1498
1578
  `being told about it rather than shown it. Describe what you expected to verify ` +
1499
1579
  `and ask the user what they see, or switch to a model with vision using /model.`,
1500
1580
  synthetic: true,
@@ -1784,9 +1864,9 @@ const CALL_LOG_LIMIT = 200;
1784
1864
  /** One call's usage, flattened for the session file. Exported so the recording is
1785
1865
  * testable on its own — persisting a hand-built record proves nothing about what the
1786
1866
  * engine actually writes. */
1787
- export function toCallRecord(u, model) {
1867
+ export function toCallRecord(u, model, at = Date.now()) {
1788
1868
  return {
1789
- at: Date.now(),
1869
+ at,
1790
1870
  prompt: u.promptTokens,
1791
1871
  hit: u.cacheHitTokens,
1792
1872
  miss: u.cacheMissTokens,