mindweave 2.4.8 → 2.5.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 +4 -2
- package/dist/cli/App.js +101 -27
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/commandArgs.js +66 -16
- package/dist/cli/commandArgs.js.map +1 -1
- package/dist/cli/commands.js +1 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/components/ApprovalBox.js +10 -1
- package/dist/cli/components/ApprovalBox.js.map +1 -1
- package/dist/cli/components/McpMinitabs.js +1 -1
- package/dist/cli/components/Picker.js +61 -19
- package/dist/cli/components/Picker.js.map +1 -1
- package/dist/cli/components/ToolLine.js +1 -1
- package/dist/cli/components/ToolLine.js.map +1 -1
- package/dist/cli/feedback.js +223 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/framebuffer/writer.js +1 -1
- package/dist/cli/toolDisplay.js +4 -3
- package/dist/cli/toolDisplay.js.map +1 -1
- package/dist/drivers/clientId.js +2 -3
- package/dist/drivers/clientId.js.map +1 -1
- package/dist/drivers/openaiCompat/wire.js +55 -3
- package/dist/drivers/openaiCompat/wire.js.map +1 -1
- package/dist/drivers/openrouter/catalog.js +136 -0
- package/dist/drivers/openrouter/catalog.js.map +1 -0
- package/dist/drivers/openrouter/client.js +102 -0
- package/dist/drivers/openrouter/client.js.map +1 -0
- package/dist/drivers/openrouter/endpoint.js +20 -0
- package/dist/drivers/openrouter/endpoint.js.map +1 -0
- package/dist/drivers/openrouter/index.js +5 -0
- package/dist/drivers/openrouter/index.js.map +1 -0
- package/dist/drivers/openrouter/manifest.js +83 -0
- package/dist/drivers/openrouter/manifest.js.map +1 -0
- package/dist/drivers/providerError.js +51 -0
- package/dist/drivers/providerError.js.map +1 -1
- package/dist/drivers/registry.js +111 -3
- package/dist/drivers/registry.js.map +1 -1
- package/dist/dynamo/engine.js +152 -99
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/dynamo/model.js +63 -4
- package/dist/dynamo/model.js.map +1 -1
- package/dist/governor/skills.js +1 -1
- package/dist/governor/write.js +61 -0
- package/dist/governor/write.js.map +1 -1
- package/dist/mcp/configWrite.js +1 -1
- package/dist/memory/compaction.js +9 -1
- package/dist/memory/compaction.js.map +1 -1
- package/dist/memory/store.js +8 -1
- package/dist/memory/store.js.map +1 -1
- package/dist/tools/backgroundShells.js +4 -3
- package/dist/tools/backgroundShells.js.map +1 -1
- package/dist/tools/deferredNative.js +13 -4
- package/dist/tools/deferredNative.js.map +1 -1
- package/dist/tools/governorTools.js +113 -9
- package/dist/tools/governorTools.js.map +1 -1
- package/dist/tools/mcpAdd.js +87 -4
- package/dist/tools/mcpAdd.js.map +1 -1
- package/dist/tools/mcpSearch.js +5 -4
- package/dist/tools/mcpSearch.js.map +1 -1
- package/dist/tools/mindweaveStatus.js +113 -0
- package/dist/tools/mindweaveStatus.js.map +1 -0
- package/dist/tools/nativeStderr.js +27 -0
- package/dist/tools/nativeStderr.js.map +1 -0
- package/dist/tools/registry.js +6 -4
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/runCommand.js +53 -18
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/screenshot.js +17 -4
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/todo.js +5 -5
- package/dist/tools/todo.js.map +1 -1
- package/package.json +4 -3
package/dist/dynamo/engine.js
CHANGED
|
@@ -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
|
|
@@ -507,8 +516,8 @@ directoryNotes = []) {
|
|
|
507
516
|
: []),
|
|
508
517
|
...(missing.length > 0 ? [`${missing.join(", ")} could not be read from disk`] : []),
|
|
509
518
|
];
|
|
510
|
-
const content = notes.length > 0 ? `${said}
|
|
511
|
-
|
|
519
|
+
const content = notes.length > 0 ? `${said}
|
|
520
|
+
|
|
512
521
|
[${notes.join("; ")}]` : said;
|
|
513
522
|
messages.push({ role: "user", content, ...(images.length > 0 ? { images } : {}) });
|
|
514
523
|
continue;
|
|
@@ -543,7 +552,12 @@ directoryNotes = []) {
|
|
|
543
552
|
approvedAt: session.toolContext.activePlanApprovedAt ?? "",
|
|
544
553
|
mode: "lightning",
|
|
545
554
|
})
|
|
546
|
-
: "", directoryNotes
|
|
555
|
+
: "", directoryNotes,
|
|
556
|
+
// Only while the turn is still sitting where the reset put it. Once a command moves,
|
|
557
|
+
// the command's own result says where it is now.
|
|
558
|
+
session.toolContext.cwdResetFrom && session.toolContext.cwd === session.cwd
|
|
559
|
+
? relativize(session.toolContext, session.toolContext.cwdResetFrom)
|
|
560
|
+
: ""),
|
|
547
561
|
tools,
|
|
548
562
|
model: session.modelConfig,
|
|
549
563
|
};
|
|
@@ -558,55 +572,83 @@ async function backgroundEventNotes(session) {
|
|
|
558
572
|
if (!mgr)
|
|
559
573
|
return [];
|
|
560
574
|
const events = await mgr.drainEvents();
|
|
561
|
-
return events.map((
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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` +
|
|
575
|
+
return events.map(backgroundEventNote).filter((note) => note !== null);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The note for one background-shell event, or null when there is nothing to say (pure).
|
|
579
|
+
*
|
|
580
|
+
* An ending the AGENT caused says nothing: `kill_shell` already told it the shell
|
|
581
|
+
* stopped. The note that used to follow declared "this is the user stopping their own
|
|
582
|
+
* app", which blamed the user for the agent's own restart and gave the model a second,
|
|
583
|
+
* contradictory account of the same event.
|
|
584
|
+
*/
|
|
585
|
+
export function backgroundEventNote({ info, kind, tail, wake, }) {
|
|
586
|
+
// It came up. This is the only positive event a server ever produces, and it is
|
|
587
|
+
// what lets the model actually deliver the "I'll tell you when it's running" it
|
|
588
|
+
// was told to say. Nothing has gone wrong, so there is nothing to fix.
|
|
589
|
+
if (kind === "ready") {
|
|
590
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) is up and running.]\n` +
|
|
607
591
|
`Recent output:\n${tail || "(no output)"}\n\n` +
|
|
608
|
-
|
|
609
|
-
|
|
592
|
+
`Tell the user in one short line that it's running. Nothing is wrong — do not investigate, ` +
|
|
593
|
+
`do not restart it, and do not change any files because of this. Running only means the ` +
|
|
594
|
+
`process started: do not describe what it shows or say a change is visible unless you ` +
|
|
595
|
+
`have actually looked.`);
|
|
596
|
+
}
|
|
597
|
+
// The watchdog thinks this running shell is stuck. It has produced nothing for a
|
|
598
|
+
// while — either blocked on a prompt it will never answer, or silently wedged on a
|
|
599
|
+
// command that should have kept working. The point is to stop it sitting invisible
|
|
600
|
+
// until the timeout, and to hand the model the two moves that resolve it.
|
|
601
|
+
if (kind === "stalled") {
|
|
602
|
+
const why = info.stallReason === "prompt"
|
|
603
|
+
? `It looks like it is waiting for interactive input (its last line reads as a prompt). ` +
|
|
604
|
+
`Kill it with kill_shell #${info.id} and re-run non-interactively — pipe the answer in ` +
|
|
605
|
+
`(e.g. \`echo y | …\`) or add a non-interactive flag like \`-y\`/\`--yes\`.`
|
|
606
|
+
: `It has produced no output for a long time and may be wedged. Read it with shells #${info.id} ` +
|
|
607
|
+
`to judge, then either keep waiting if it is genuinely mid-work, or kill it with ` +
|
|
608
|
+
`kill_shell #${info.id} and look into why it hangs.`;
|
|
609
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) appears to be stuck.]\n` +
|
|
610
|
+
`Recent output:\n${tail || "(no output)"}\n\n${why}`);
|
|
611
|
+
}
|
|
612
|
+
const status = info.status === "killed"
|
|
613
|
+
? info.stoppedBy === "user"
|
|
614
|
+
? "was stopped by the user"
|
|
615
|
+
: "was killed"
|
|
616
|
+
: `finished with exit code ${info.exitCode}`;
|
|
617
|
+
// An ending that is NOT worth interrupting for still arrives, so the model knows the
|
|
618
|
+
// thing is down and can answer about it. It is explicitly not a task: this is the
|
|
619
|
+
// path a user closing their own app takes, and treating it as news is what made the
|
|
620
|
+
// agent reopen it.
|
|
621
|
+
if (info.status === "killed" && info.stoppedBy === "agent")
|
|
622
|
+
return null;
|
|
623
|
+
// Mindweave stopped it itself, because its output ran past the size cap. That is a
|
|
624
|
+
// runaway, not someone closing an app, and the model is the one who can explain it.
|
|
625
|
+
if (info.status === "killed" && info.stoppedBy === "system") {
|
|
626
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) was stopped by Mindweave because its output ` +
|
|
627
|
+
`passed the size limit.]\n` +
|
|
628
|
+
`Recent output:\n${tail || "(no output)"}\n\n` +
|
|
629
|
+
`Something in it was writing without end. Tell the user, and look at the output above before ` +
|
|
630
|
+
`running it again.`);
|
|
631
|
+
}
|
|
632
|
+
if (!wake) {
|
|
633
|
+
// Only a stop the user made through the app is known to be theirs. An app that exited
|
|
634
|
+
// on its own after coming up was most likely closed by them, which is worth saying as
|
|
635
|
+
// the likely reading rather than as a fact.
|
|
636
|
+
const who = info.status === "killed" && info.stoppedBy === "user"
|
|
637
|
+
? "the user stopping their own app"
|
|
638
|
+
: "most likely the user closing their own app";
|
|
639
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) ${status}. It had already started up, so ` +
|
|
640
|
+
`this is ${who}, not a failure.]\n` +
|
|
641
|
+
`This is background information only. Do NOT mention it unless it is relevant, do NOT restart ` +
|
|
642
|
+
`it, and do NOT change any files because of it. If the user later asks about this app, you now ` +
|
|
643
|
+
`know it is stopped.`);
|
|
644
|
+
}
|
|
645
|
+
// For a server, only a failure to come up reaches here: a normal stop does not wake.
|
|
646
|
+
const guidance = info.notify === "on_failure"
|
|
647
|
+
? "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."
|
|
648
|
+
: "If it failed, tell the user briefly what went wrong and propose a fix — don't change files unless they agree.";
|
|
649
|
+
return (`[Background shell #${info.id} (\`${info.command}\`) ${status}.]\n` +
|
|
650
|
+
`Recent output:\n${tail || "(no output)"}\n\n` +
|
|
651
|
+
guidance);
|
|
610
652
|
}
|
|
611
653
|
/**
|
|
612
654
|
* Produce Mindweave's next reply for the latest user message already on
|
|
@@ -696,13 +738,13 @@ export async function respond(session, options = {}) {
|
|
|
696
738
|
function implementFromScratch(priorPath, plan) {
|
|
697
739
|
const path = priorPath;
|
|
698
740
|
const where = path
|
|
699
|
-
? `
|
|
700
|
-
|
|
741
|
+
? `
|
|
742
|
+
|
|
701
743
|
If you need something exact from the planning that produced this — a snippet, an ` +
|
|
702
744
|
`error message, a path — the full conversation is at: ${path}`
|
|
703
745
|
: "";
|
|
704
|
-
return `Implement the following plan:
|
|
705
|
-
|
|
746
|
+
return `Implement the following plan:
|
|
747
|
+
|
|
706
748
|
${plan}${where}`;
|
|
707
749
|
}
|
|
708
750
|
/**
|
|
@@ -803,12 +845,16 @@ async function respondTurn(session, options = {}) {
|
|
|
803
845
|
// (its lifecycle + tagged tool calls) up this same stream instead of running dark.
|
|
804
846
|
session.toolContext.emitEvent = options.onEvent;
|
|
805
847
|
session.toolContext.abortSignal = options.signal;
|
|
848
|
+
// So a tool can answer "which model are you running" instead of guessing.
|
|
849
|
+
session.toolContext.modelConfig = session.modelConfig;
|
|
806
850
|
// WORKING-DIRECTORY RESET. Each turn starts at the project root — the working
|
|
807
851
|
// directory is already set to the correct project directory automatically. Within a
|
|
808
852
|
// turn cd still persists (so a multi-step command sequence works), but it never
|
|
809
853
|
// carries a stale `cd` into the next
|
|
810
854
|
// turn — the bug where `cd src-tauri` run in two turns became `…/src-tauri/src-tauri`.
|
|
811
855
|
// The primary root (session.cwd) is fixed; only toolContext.cwd moves.
|
|
856
|
+
const leftIn = session.toolContext.cwd;
|
|
857
|
+
session.toolContext.cwdResetFrom = leftIn && leftIn !== session.cwd ? leftIn : undefined;
|
|
812
858
|
session.toolContext.cwd = session.cwd;
|
|
813
859
|
// TASK-BOUNDARY SWEEP. If the previous turn finished a task (a todo list completed)
|
|
814
860
|
// and this new message opens a DIFFERENT one (not a "continue"), close the finished
|
|
@@ -836,6 +882,10 @@ async function respondTurn(session, options = {}) {
|
|
|
836
882
|
const limits = taskLimits();
|
|
837
883
|
const startedAt = Date.now();
|
|
838
884
|
const usages = [];
|
|
885
|
+
// When each of those calls returned. Recorded as it happens: stamping them when the
|
|
886
|
+
// turn is saved gave every call in a turn the same time, so the call log could not say
|
|
887
|
+
// how a long turn's time was spent (one real session: 145 calls, 4 distinct times).
|
|
888
|
+
const usageTimes = [];
|
|
839
889
|
// Verification-gate bookkeeping for this turn: did the model change any file,
|
|
840
890
|
// did it ever run a check, and have we already nudged once (one-shot).
|
|
841
891
|
let mutatedThisTurn = false;
|
|
@@ -898,7 +948,7 @@ async function respondTurn(session, options = {}) {
|
|
|
898
948
|
// of them — those are different problems with different fixes, and the totals look
|
|
899
949
|
// identical for all of them. Six numbers per call, capped, so a long session cannot
|
|
900
950
|
// grow the meta file without bound.
|
|
901
|
-
session.callLog = [...(session.callLog ?? []), ...usages.map((u) => toCallRecord(u, session.modelConfig.model))].slice(-CALL_LOG_LIMIT);
|
|
951
|
+
session.callLog = [...(session.callLog ?? []), ...usages.map((u, i) => toCallRecord(u, session.modelConfig.model, usageTimes[i]))].slice(-CALL_LOG_LIMIT);
|
|
902
952
|
};
|
|
903
953
|
try {
|
|
904
954
|
const reply = await runTurn();
|
|
@@ -1070,6 +1120,7 @@ async function respondTurn(session, options = {}) {
|
|
|
1070
1120
|
emitUsage(result, options);
|
|
1071
1121
|
if (result.usage) {
|
|
1072
1122
|
usages.push(result.usage);
|
|
1123
|
+
usageTimes.push(Date.now());
|
|
1073
1124
|
writeCacheLog(cacheCallLine({
|
|
1074
1125
|
call: usages.length,
|
|
1075
1126
|
gapMs: sinceLastCall,
|
|
@@ -1493,8 +1544,10 @@ async function respondTurn(session, options = {}) {
|
|
|
1493
1544
|
session.transcript.push({
|
|
1494
1545
|
role: "user",
|
|
1495
1546
|
content: canSee
|
|
1496
|
-
?
|
|
1497
|
-
|
|
1547
|
+
? // Not "just captured": view_image opens files that already existed (the user's own
|
|
1548
|
+
// screenshots), and saying they were captured tells the model it took them.
|
|
1549
|
+
`Here ${shots.length === 1 ? "is the image" : "are the images"} from the tool call above (${names}).`
|
|
1550
|
+
: `${names} is ready, but this model cannot see images, so you are ` +
|
|
1498
1551
|
`being told about it rather than shown it. Describe what you expected to verify ` +
|
|
1499
1552
|
`and ask the user what they see, or switch to a model with vision using /model.`,
|
|
1500
1553
|
synthetic: true,
|
|
@@ -1784,9 +1837,9 @@ const CALL_LOG_LIMIT = 200;
|
|
|
1784
1837
|
/** One call's usage, flattened for the session file. Exported so the recording is
|
|
1785
1838
|
* testable on its own — persisting a hand-built record proves nothing about what the
|
|
1786
1839
|
* engine actually writes. */
|
|
1787
|
-
export function toCallRecord(u, model) {
|
|
1840
|
+
export function toCallRecord(u, model, at = Date.now()) {
|
|
1788
1841
|
return {
|
|
1789
|
-
at
|
|
1842
|
+
at,
|
|
1790
1843
|
prompt: u.promptTokens,
|
|
1791
1844
|
hit: u.cacheHitTokens,
|
|
1792
1845
|
miss: u.cacheMissTokens,
|