march-cli 0.1.42 → 0.1.45
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/package.json +1 -1
- package/src/agent/code-search/cache.mjs +3 -2
- package/src/agent/code-search/engine.mjs +2 -0
- package/src/agent/code-search/retrieval/resilient-vectorizer.mjs +59 -0
- package/src/agent/code-search/tool.mjs +11 -5
- package/src/agent/runtime/remote-ui-client.mjs +1 -1
- package/src/agent/runtime/ui-event-bridge.mjs +2 -2
- package/src/agent/turn/turn-runner.mjs +23 -16
- package/src/cli/fallback-ui.mjs +2 -2
- package/src/cli/repl-loop.mjs +7 -7
- package/src/cli/startup/app-runtime.mjs +5 -2
- package/src/cli/tui/output/timeline-block-restore.mjs +1 -1
- package/src/cli/tui/recall-rendering.mjs +14 -7
- package/src/cli/turn/turn-input-preparer.mjs +6 -4
- package/src/cli/ui.mjs +2 -2
- package/src/cli/workspace/tui-timeline-projection.mjs +1 -1
- package/src/context/engine.mjs +2 -2
- package/src/context/system-core/base.md +1 -1
- package/src/memory/markdown/markdown-format.mjs +0 -17
- package/src/memory/markdown/markdown-recall.mjs +11 -19
- package/src/memory/markdown/semantic-preload.mjs +17 -0
- package/src/memory/markdown/semantic-recall.mjs +165 -0
- package/src/memory/markdown/sqlite-index.mjs +1 -13
- package/src/memory/markdown-store.mjs +24 -52
- package/src/web-ui/dist/assets/{index-DrlJis_D.js → index-CBYbNVgs.js} +1 -1
- package/src/web-ui/dist/assets/{index-BQtl1uQs.css → index-CcbYCcWs.css} +1 -1
- package/src/web-ui/dist/index.html +2 -2
- package/src/web-ui/runtime-host.mjs +5 -2
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +24 -0
- package/src/web-ui/src/model.ts +18 -0
- package/src/web-ui/src/runtime/client.ts +2 -1
- package/src/web-ui/src/runtime/runtimeTimeline.ts +5 -0
- package/src/web-ui/src/styles/shell.css +6 -0
- package/src/web-ui/src/timelineAdapter.ts +2 -0
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { chunkFile } from "./chunker.mjs";
|
|
4
4
|
import { Bm25Index } from "./retrieval/bm25.mjs";
|
|
5
|
+
import { describeVectorizer } from "./retrieval/resilient-vectorizer.mjs";
|
|
5
6
|
import { LocalVectorIndex, defaultVectorizer } from "./retrieval/vector.mjs";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_MAX_FILE_ENTRIES = 8_000;
|
|
@@ -53,7 +54,7 @@ export class CodeSearchIndexCache {
|
|
|
53
54
|
if (cachedIndex) {
|
|
54
55
|
this.indices.delete(indexSignature);
|
|
55
56
|
this.indices.set(indexSignature, cachedIndex);
|
|
56
|
-
return { chunks, index: cachedIndex, reusedFiles, indexedFiles, reusedIndex: true,
|
|
57
|
+
return { chunks, index: cachedIndex, reusedFiles, indexedFiles, reusedIndex: true, ...describeVectorizer(this.vectorizer) };
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const index = {
|
|
@@ -62,7 +63,7 @@ export class CodeSearchIndexCache {
|
|
|
62
63
|
};
|
|
63
64
|
this.indices.set(indexSignature, index);
|
|
64
65
|
this.pruneIndexCache();
|
|
65
|
-
return { chunks, index, reusedFiles, indexedFiles, reusedIndex: false,
|
|
66
|
+
return { chunks, index, reusedFiles, indexedFiles, reusedIndex: false, ...describeVectorizer(this.vectorizer) };
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
clear() {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { HashingVectorizer } from "./vector.mjs";
|
|
2
|
+
|
|
3
|
+
export class ResilientVectorizer {
|
|
4
|
+
constructor({ primary, fallback = new HashingVectorizer(), label = "embedding" } = {}) {
|
|
5
|
+
if (!primary) throw new Error("ResilientVectorizer requires primary");
|
|
6
|
+
this.primary = primary;
|
|
7
|
+
this.fallback = fallback;
|
|
8
|
+
this.label = label;
|
|
9
|
+
this.id = `resilient:${primary.id}->${fallback.id}`;
|
|
10
|
+
this.active = primary;
|
|
11
|
+
this.status = "primary";
|
|
12
|
+
this.warning = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get dimensions() {
|
|
16
|
+
return this.active.dimensions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get activeId() {
|
|
20
|
+
return this.active.id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async load() {
|
|
24
|
+
if (this.status === "fallback") return false;
|
|
25
|
+
try {
|
|
26
|
+
if (typeof this.primary.load === "function") await this.primary.load();
|
|
27
|
+
else await this.primary.encode([`${this.label} warmup`]);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
this.#activateFallback(err);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async encode(texts) {
|
|
36
|
+
if (this.status === "fallback") return this.fallback.encode(texts);
|
|
37
|
+
try {
|
|
38
|
+
return await this.primary.encode(texts);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
this.#activateFallback(err);
|
|
41
|
+
return this.fallback.encode(texts);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#activateFallback(err) {
|
|
46
|
+
this.active = this.fallback;
|
|
47
|
+
this.status = "fallback";
|
|
48
|
+
const message = err?.message ?? String(err);
|
|
49
|
+
this.warning = `${this.label} Model2Vec unavailable; using local hashing fallback: ${message}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function describeVectorizer(vectorizer) {
|
|
54
|
+
return {
|
|
55
|
+
vectorizer: vectorizer?.activeId ?? vectorizer?.id ?? "unknown",
|
|
56
|
+
vectorizer_status: vectorizer?.status ?? "primary",
|
|
57
|
+
vectorizer_warning: vectorizer?.warning ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -5,6 +5,7 @@ import { toolText } from "../tool-result.mjs";
|
|
|
5
5
|
import { CodeSearchIndexCache } from "./cache.mjs";
|
|
6
6
|
import { searchCode } from "./engine.mjs";
|
|
7
7
|
import { Model2VecVectorizer, POTION_CODE_MODEL_ID } from "./retrieval/model2vec.mjs";
|
|
8
|
+
import { ResilientVectorizer } from "./retrieval/resilient-vectorizer.mjs";
|
|
8
9
|
|
|
9
10
|
const persistentCaches = new Map();
|
|
10
11
|
|
|
@@ -23,7 +24,7 @@ export function createCodeSearchTool({ engine, stateRoot = null }) {
|
|
|
23
24
|
Type.Literal("symbol"),
|
|
24
25
|
Type.Literal("lexical"),
|
|
25
26
|
Type.Literal("semantic"),
|
|
26
|
-
], { description: "Search mode. auto uses BM25 + Model2Vec retrieval with RRF fusion." })),
|
|
27
|
+
], { description: "Search mode. auto uses BM25 + Model2Vec retrieval with RRF fusion; falls back to local hashing if the model is unavailable." })),
|
|
27
28
|
include_tests: Type.Optional(Type.Boolean({ description: "Include test/spec paths without penalty; default false" })),
|
|
28
29
|
related_to: Type.Optional(Type.Object({
|
|
29
30
|
file_path: Type.String({ description: "Workspace-relative file path containing the known code" }),
|
|
@@ -53,7 +54,10 @@ function persistentCacheFor(stateRoot) {
|
|
|
53
54
|
if (!cache) {
|
|
54
55
|
cache = new CodeSearchIndexCache({
|
|
55
56
|
storagePath,
|
|
56
|
-
vectorizer: new
|
|
57
|
+
vectorizer: new ResilientVectorizer({
|
|
58
|
+
primary: new Model2VecVectorizer({ modelDir }),
|
|
59
|
+
label: "code_search",
|
|
60
|
+
}),
|
|
57
61
|
});
|
|
58
62
|
persistentCaches.set(cacheKey, cache);
|
|
59
63
|
}
|
|
@@ -61,13 +65,15 @@ function persistentCacheFor(stateRoot) {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
function formatSearchOutput({ results, stats }) {
|
|
64
|
-
const
|
|
65
|
-
|
|
68
|
+
const mode = stats.vectorizer_status === "fallback" ? `${stats.mode}-fallback` : stats.mode;
|
|
69
|
+
const header = `--- code_search (${results.length} results, ${stats.files} files, ${stats.chunks} chunks, ${mode}) ---`;
|
|
70
|
+
const warning = stats.vectorizer_warning ? `\nwarning: ${stats.vectorizer_warning}` : "";
|
|
71
|
+
if (results.length === 0) return `${header}${warning}\nNo matching code snippets found.`;
|
|
66
72
|
const body = results.map((result, index) => [
|
|
67
73
|
`${index + 1}. ${result.file_path}:${result.start_line}-${result.end_line} score=${result.score} kind=${result.kind}${result.symbols.length ? ` symbols=${result.symbols.join(",")}` : ""}`,
|
|
68
74
|
fenceSnippet(result.snippet),
|
|
69
75
|
].join("\n")).join("\n\n");
|
|
70
|
-
return `${header}\n${body}`;
|
|
76
|
+
return `${header}${warning}\n${body}`;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
function fenceSnippet(snippet) {
|
|
@@ -13,7 +13,7 @@ export function createRemoteRuntimeUiClient(peer) {
|
|
|
13
13
|
retryEnd: (event) => peer.notify("uiEvent", { type: "retry_end", ...event }),
|
|
14
14
|
status: (text) => peer.notify("uiEvent", { type: "status", text }),
|
|
15
15
|
debugLines: (lines) => peer.notify("uiEvent", { type: "debug_lines", lines }),
|
|
16
|
-
recall: ({ source, hints }) => peer.notify("uiEvent", { type: "recall", source, hints }),
|
|
16
|
+
recall: ({ source, hints, report }) => peer.notify("uiEvent", { type: "recall", source, hints, report }),
|
|
17
17
|
editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -50,7 +50,7 @@ export function createRuntimeUiClient(eventBus) {
|
|
|
50
50
|
retryEnd: (event) => eventBus.emit({ type: "retry_end", ...event }),
|
|
51
51
|
status: (text) => eventBus.emit({ type: "status", text }),
|
|
52
52
|
debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
|
|
53
|
-
recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
|
|
53
|
+
recall: ({ source, hints, report }) => eventBus.emit({ type: "recall", source, hints, report }),
|
|
54
54
|
providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
|
|
55
55
|
editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
|
|
56
56
|
};
|
|
@@ -71,7 +71,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
|
|
|
71
71
|
case "retry_end": return ui.retryEnd?.(pickRetryEnd(event));
|
|
72
72
|
case "status": return ui.status?.(event.text);
|
|
73
73
|
case "debug_lines": return writeDebugLines(ui, event.lines);
|
|
74
|
-
case "recall": return ui.recall?.({
|
|
74
|
+
case "recall": return ui.recall?.({ hints: event.hints, report: event.report });
|
|
75
75
|
case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
|
|
76
76
|
case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
|
|
77
77
|
default: return undefined;
|
|
@@ -23,11 +23,11 @@ export async function runRunnerTurn({
|
|
|
23
23
|
}) {
|
|
24
24
|
const {
|
|
25
25
|
userRecallHints = [],
|
|
26
|
-
currentProject = "",
|
|
27
26
|
} = options;
|
|
28
27
|
const activeSession = sessionBinding.get();
|
|
29
28
|
const turnState = createTurnEventState();
|
|
30
29
|
const midTurnRecallHints = [];
|
|
30
|
+
const midTurnRecallTasks = [];
|
|
31
31
|
const idleWatchdog = createModelStreamIdleWatchdog({ session: activeSession, logger, setPhase });
|
|
32
32
|
ui.turnStart();
|
|
33
33
|
setPhase?.("subscribed");
|
|
@@ -57,12 +57,8 @@ export async function runRunnerTurn({
|
|
|
57
57
|
}
|
|
58
58
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
59
59
|
if (event.type === "tool_execution_start") {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
midTurnRecallHints.push(...hints);
|
|
63
|
-
queueMidTurnRecallHints(activeSession, hints, logger);
|
|
64
|
-
ui.recall?.({ source: "assistant", hints });
|
|
65
|
-
}
|
|
60
|
+
const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints });
|
|
61
|
+
midTurnRecallTasks.push(task);
|
|
66
62
|
}
|
|
67
63
|
});
|
|
68
64
|
|
|
@@ -90,11 +86,11 @@ export async function runRunnerTurn({
|
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
setPhase?.("finalizing");
|
|
93
|
-
|
|
89
|
+
await Promise.allSettled(midTurnRecallTasks);
|
|
90
|
+
await finalizeTurn({
|
|
94
91
|
prompt,
|
|
95
92
|
userMessage,
|
|
96
93
|
userRecallHints,
|
|
97
|
-
currentProject,
|
|
98
94
|
memoryStore,
|
|
99
95
|
engine,
|
|
100
96
|
ui,
|
|
@@ -172,13 +168,13 @@ function throwIfAssistantEndedWithError(turnState) {
|
|
|
172
168
|
}
|
|
173
169
|
|
|
174
170
|
function queueMidTurnRecallHints(session, hints, logger) {
|
|
175
|
-
const content = formatRecallHints(
|
|
171
|
+
const content = formatRecallHints(hints);
|
|
176
172
|
if (!content) return;
|
|
177
173
|
const injected = session.sendCustomMessage?.({
|
|
178
174
|
customType: "march.recall",
|
|
179
175
|
content,
|
|
180
176
|
display: false,
|
|
181
|
-
details: {
|
|
177
|
+
details: { type: "recall" },
|
|
182
178
|
}, { deliverAs: "steer" });
|
|
183
179
|
void injected?.catch?.((err) => {
|
|
184
180
|
logger?.debug("memory.mid_turn_recall.inject_failed", { errorMessage: err?.message ?? String(err) });
|
|
@@ -209,9 +205,9 @@ function logSessionEvent(logger, event) {
|
|
|
209
205
|
});
|
|
210
206
|
}
|
|
211
207
|
|
|
212
|
-
function finalizeTurn({ prompt, userMessage, userRecallHints,
|
|
208
|
+
async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
|
|
213
209
|
closeAssistantReply({ ui, state: turnState });
|
|
214
|
-
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState
|
|
210
|
+
const assistantRecallHints = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
215
211
|
engine.setPendingAssistantRecallHints?.(assistantRecallHints);
|
|
216
212
|
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
|
|
217
213
|
|
|
@@ -228,17 +224,28 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
|
|
|
228
224
|
syncCurrentMarchSessionState();
|
|
229
225
|
}
|
|
230
226
|
|
|
231
|
-
function flushAssistantRecall({ memoryStore, engine, turnState
|
|
227
|
+
async function flushAssistantRecall({ memoryStore, engine, turnState }) {
|
|
232
228
|
if (!memoryStore) return [];
|
|
233
229
|
const text = assistantRecallDeltaText(turnState);
|
|
234
230
|
advanceAssistantRecallCursor(turnState);
|
|
235
231
|
if (!text.trim()) return [];
|
|
236
|
-
return memoryStore.recallForAssistant(text, {
|
|
237
|
-
currentProject,
|
|
232
|
+
return await memoryStore.recallForAssistant(text, {
|
|
238
233
|
excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
|
|
239
234
|
});
|
|
240
235
|
}
|
|
241
236
|
|
|
237
|
+
async function flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints }) {
|
|
238
|
+
try {
|
|
239
|
+
const hints = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
240
|
+
if (hints.length === 0) return;
|
|
241
|
+
midTurnRecallHints.push(...hints);
|
|
242
|
+
queueMidTurnRecallHints(activeSession, hints, logger);
|
|
243
|
+
ui.recall?.({ hints });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
logger?.debug("memory.mid_turn_recall.failed", { errorMessage: err?.message ?? String(err) });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
242
249
|
function assistantRecallDeltaText(turnState) {
|
|
243
250
|
const cursor = turnState.recallCursor ?? { draftLength: 0, thinkingLength: 0 };
|
|
244
251
|
const thinking = assistantThinkingText(turnState);
|
package/src/cli/fallback-ui.mjs
CHANGED
|
@@ -109,9 +109,9 @@ export function createPlainUI() {
|
|
|
109
109
|
},
|
|
110
110
|
textDelta: writeText,
|
|
111
111
|
status: (text) => { ensureNewline(); stdout.write(`${brightBlack(`● ${text}`)}\n`); },
|
|
112
|
-
recall: ({ hints }) => {
|
|
112
|
+
recall: ({ hints, report }) => {
|
|
113
113
|
ensureNewline();
|
|
114
|
-
for (const line of formatRecallLines(hints)) stdout.write(`${brightBlack(line)}\n`);
|
|
114
|
+
for (const line of formatRecallLines(hints, report)) stdout.write(`${brightBlack(line)}\n`);
|
|
115
115
|
},
|
|
116
116
|
clearOutput: () => {},
|
|
117
117
|
restoreTranscript: () => {},
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -16,10 +16,10 @@ export async function runSingleShotPrompt({
|
|
|
16
16
|
}) {
|
|
17
17
|
memoryStore.beginTurn();
|
|
18
18
|
try {
|
|
19
|
-
const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
19
|
+
const turnInput = await prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
20
20
|
ui.writeln(turnInput.displayMessage);
|
|
21
|
-
ui.recall?.({
|
|
22
|
-
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({
|
|
21
|
+
ui.recall?.({ hints: turnInput.userRecallHints, report: turnInput.userRecallReport });
|
|
22
|
+
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ hints: turnInput.carryoverRecallHints });
|
|
23
23
|
refreshStatusBar.startWorking?.();
|
|
24
24
|
const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
|
|
25
25
|
renderPendingAssistantRecallPreview({ runner, ui });
|
|
@@ -185,10 +185,10 @@ function startReplTurn({ runtime, prompt, ui, refreshStatusBar, setTurnRunning,
|
|
|
185
185
|
async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
|
|
186
186
|
memoryStore.beginTurn();
|
|
187
187
|
try {
|
|
188
|
-
const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
188
|
+
const turnInput = await prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
189
189
|
ui.writeln(turnInput.displayMessage);
|
|
190
|
-
ui.recall?.({
|
|
191
|
-
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({
|
|
190
|
+
ui.recall?.({ hints: turnInput.userRecallHints, report: turnInput.userRecallReport });
|
|
191
|
+
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ hints: turnInput.carryoverRecallHints });
|
|
192
192
|
setTurnRunning(true);
|
|
193
193
|
refreshStatusBar.startWorking?.();
|
|
194
194
|
const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
|
|
@@ -216,6 +216,6 @@ function renderPendingAssistantRecallPreview({ runner, ui }) {
|
|
|
216
216
|
if (runner.engine.hasRenderedPendingAssistantRecallHints?.()) return;
|
|
217
217
|
const hints = runner.engine.peekPendingAssistantRecallHints?.() ?? [];
|
|
218
218
|
if (hints.length === 0) return;
|
|
219
|
-
ui.recall?.({
|
|
219
|
+
ui.recall?.({ hints });
|
|
220
220
|
runner.engine.markPendingAssistantRecallHintsRendered?.();
|
|
221
221
|
}
|
|
@@ -11,6 +11,7 @@ import { createMarchAuthStorage } from "../../auth/storage.mjs";
|
|
|
11
11
|
import { createRuntimeRunner } from "./create-runtime-runner.mjs";
|
|
12
12
|
import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
|
|
13
13
|
import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
|
|
14
|
+
import { preloadSemanticMemoryRecall } from "../../memory/markdown/semantic-preload.mjs";
|
|
14
15
|
import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
|
|
15
16
|
import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
|
|
16
17
|
import { loadOrCreateProjectId, resumeStartupSession } from "./startup-session.mjs";
|
|
@@ -68,7 +69,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
68
69
|
const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
|
|
69
70
|
const profilePaths = defaultProfilePaths();
|
|
70
71
|
ensureProfileFiles(profilePaths);
|
|
71
|
-
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
|
|
72
|
+
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot, stateRoot });
|
|
72
73
|
const remoteMemorySources = normalizeRemoteMemorySources(config);
|
|
73
74
|
const currentProject = basename(cwd);
|
|
74
75
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
@@ -90,6 +91,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
90
91
|
shellRuntime,
|
|
91
92
|
historyStore: inputHistoryStore,
|
|
92
93
|
});
|
|
94
|
+
await preloadSemanticMemoryRecall({ memoryStore, ui, logger });
|
|
95
|
+
|
|
93
96
|
const outputRouter = createWorkspaceOutputRouter({
|
|
94
97
|
ui,
|
|
95
98
|
activeProjectId: currentProjectInfo.projectId,
|
|
@@ -166,7 +169,7 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot }
|
|
|
166
169
|
stateRoot,
|
|
167
170
|
memoryRoot,
|
|
168
171
|
profilePaths,
|
|
169
|
-
createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot }),
|
|
172
|
+
createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot, stateRoot }),
|
|
170
173
|
provider,
|
|
171
174
|
serviceTier,
|
|
172
175
|
model,
|
|
@@ -30,7 +30,7 @@ function appendTimelineBlock(output, block) {
|
|
|
30
30
|
output.addBlock({ type: "status", lines: [String(block.content ?? "")] });
|
|
31
31
|
break;
|
|
32
32
|
case "recall":
|
|
33
|
-
output.addBlock({ type: "plain", lines: formatRecallLines(block.hints ?? []) });
|
|
33
|
+
output.addBlock({ type: "plain", lines: formatRecallLines(block.hints ?? [], block.report ?? null) });
|
|
34
34
|
break;
|
|
35
35
|
case "editDiff":
|
|
36
36
|
output.addBlock({ type: "diff", path: block.path, diffLines: block.diffLines ?? [] });
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
import { formatScore } from "../../memory/markdown/markdown-recall.mjs";
|
|
1
2
|
import { brightBlack } from "./ui-theme.mjs";
|
|
2
3
|
|
|
3
4
|
const RECALL_ICON = "✦";
|
|
4
5
|
|
|
5
|
-
export function formatRecallLines(hints = []) {
|
|
6
|
-
|
|
6
|
+
export function formatRecallLines(hints = [], report = null) {
|
|
7
|
+
const candidates = report?.candidates ?? [];
|
|
8
|
+
if (!hints.length && !candidates.length) return [];
|
|
7
9
|
const noun = hints.length === 1 ? "note" : "notes";
|
|
10
|
+
const threshold = Number.isFinite(report?.threshold) ? ` · threshold ${formatScore(report.threshold)}` : "";
|
|
11
|
+
const fallback = report?.vectorizerStatus === "fallback" ? " · fallback" : "";
|
|
8
12
|
return [
|
|
9
|
-
`${RECALL_ICON} Memory Recall · ${hints.length} ${noun}`,
|
|
10
|
-
...
|
|
13
|
+
`${RECALL_ICON} Memory Recall · ${hints.length} ${noun}${threshold}${fallback}`,
|
|
14
|
+
...(report?.warning ? [` ! ${report.warning}`] : []),
|
|
15
|
+
...(candidates.length ? candidates : hints.map((hint) => ({ ...hint, recalled: true }))).flatMap(formatHintLines),
|
|
11
16
|
];
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
export function writeRecall({ output, hints = [] }) {
|
|
15
|
-
const lines = formatRecallLines(hints);
|
|
19
|
+
export function writeRecall({ output, hints = [], report = null }) {
|
|
20
|
+
const lines = formatRecallLines(hints, report);
|
|
16
21
|
lines.forEach((line) => {
|
|
17
22
|
if (line.startsWith(" ")) output.writeln(brightBlack(line));
|
|
18
23
|
else output.writeln(line);
|
|
@@ -21,7 +26,9 @@ export function writeRecall({ output, hints = [] }) {
|
|
|
21
26
|
|
|
22
27
|
function formatHintLines(hint) {
|
|
23
28
|
const title = hint.name || hint.id || "Untitled memory";
|
|
24
|
-
const
|
|
29
|
+
const mark = hint.recalled === false ? "×" : "✓";
|
|
30
|
+
const score = `${formatScore(hint.score)} `;
|
|
31
|
+
const lines = [` ${mark} ${score}${title}`];
|
|
25
32
|
if (hint.description) lines.push(` ${hint.description}`);
|
|
26
33
|
return lines;
|
|
27
34
|
}
|
|
@@ -4,19 +4,20 @@ import { formatRecallHints } from "../../memory/markdown-store.mjs";
|
|
|
4
4
|
import { formatMessageAttachmentsForDisplay } from "../../session/attachment-display.mjs";
|
|
5
5
|
import { formatShellHints } from "../../shell/hints.mjs";
|
|
6
6
|
|
|
7
|
-
export function prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState = null }) {
|
|
7
|
+
export async function prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState = null }) {
|
|
8
8
|
const engine = runner.engine ?? {};
|
|
9
9
|
const carryoverAlreadyRendered = engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
|
|
10
10
|
const carryoverRecallHints = engine.takePendingAssistantRecallHints?.() ?? [];
|
|
11
|
-
const userRecallHints = memoryStore.recallForUser(prompt, {
|
|
11
|
+
const userRecallHints = await memoryStore.recallForUser(prompt, {
|
|
12
12
|
currentProject,
|
|
13
13
|
excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
|
|
14
14
|
});
|
|
15
|
+
const userRecallReport = memoryStore.lastUserRecallReport ?? null;
|
|
15
16
|
const modePrompt = appendModeReminder(prompt, modeState?.get?.());
|
|
16
17
|
const fullPrompt = appendPromptBlocks(
|
|
17
18
|
modePrompt,
|
|
18
|
-
formatRecallHints(
|
|
19
|
-
formatRecallHints(
|
|
19
|
+
formatRecallHints(userRecallHints),
|
|
20
|
+
formatRecallHints(carryoverRecallHints),
|
|
20
21
|
formatShellHints(runner.shellRuntime),
|
|
21
22
|
);
|
|
22
23
|
|
|
@@ -26,6 +27,7 @@ export function prepareTurnInput({ prompt, runner, memoryStore, currentProject,
|
|
|
26
27
|
runOptions: { userRecallHints, currentProject },
|
|
27
28
|
displayMessage: formatUserDisplayMessage(prompt),
|
|
28
29
|
userRecallHints,
|
|
30
|
+
userRecallReport,
|
|
29
31
|
carryoverRecallHints,
|
|
30
32
|
shouldRenderCarryoverRecall: carryoverRecallHints.length > 0 && !carryoverAlreadyRendered,
|
|
31
33
|
};
|
package/src/cli/ui.mjs
CHANGED
|
@@ -212,8 +212,8 @@ export function createTuiUI({
|
|
|
212
212
|
status: (text) => {
|
|
213
213
|
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
|
|
214
214
|
},
|
|
215
|
-
recall: ({ hints }) => {
|
|
216
|
-
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeRecall({ output, hints }); requestRender();
|
|
215
|
+
recall: ({ hints, report }) => {
|
|
216
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeRecall({ output, hints, report }); requestRender();
|
|
217
217
|
},
|
|
218
218
|
|
|
219
219
|
clearOutput: () => {
|
|
@@ -94,7 +94,7 @@ export function createTuiTimelineProjection() {
|
|
|
94
94
|
blocks.push(createBlock("status", event.at, { content: String(first ?? "") }));
|
|
95
95
|
break;
|
|
96
96
|
case "recall":
|
|
97
|
-
blocks.push(createBlock("recall", event.at, { hints: first?.hints ?? [] }));
|
|
97
|
+
blocks.push(createBlock("recall", event.at, { hints: first?.hints ?? [], report: first?.report ?? null }));
|
|
98
98
|
break;
|
|
99
99
|
case "editDiff":
|
|
100
100
|
closeAssistantBlock(event.at);
|
package/src/context/engine.mjs
CHANGED
|
@@ -168,14 +168,14 @@ export class ContextEngine {
|
|
|
168
168
|
for (const turn of this.turns) {
|
|
169
169
|
let block = `## Turn ${turn.index}\n` +
|
|
170
170
|
`[user]\n${String(turn.userMessage ?? "")}\n`;
|
|
171
|
-
const userRecall = formatRecallHints(
|
|
171
|
+
const userRecall = formatRecallHints(turn.userRecallHints ?? []);
|
|
172
172
|
if (userRecall) block += `\n${userRecall}\n`;
|
|
173
173
|
block += `\n[assistant]\n`;
|
|
174
174
|
const assistantText = turn.assistantContext || turn.assistantMessage;
|
|
175
175
|
if (assistantText) {
|
|
176
176
|
block += `\n${String(assistantText ?? "")}\n`;
|
|
177
177
|
}
|
|
178
|
-
const assistantRecall = formatRecallHints(
|
|
178
|
+
const assistantRecall = formatRecallHints(turn.assistantRecallHints ?? []);
|
|
179
179
|
if (assistantRecall) block += `\n${assistantRecall}\n`;
|
|
180
180
|
entries.push(block);
|
|
181
181
|
}
|
|
@@ -96,7 +96,7 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
|
|
|
96
96
|
</git_contract>
|
|
97
97
|
|
|
98
98
|
<memory_system>
|
|
99
|
-
- [recall
|
|
99
|
+
- [recall] blocks in recent_chat are lightweight memory hints matched by semantic recall. Treat them as possibly relevant pointers, not as complete facts.
|
|
100
100
|
- A recall hint's description may record key operational constraints, including when the full memory must be opened; factor those constraints into relevance before acting.
|
|
101
101
|
- If a recall hint may help the current task, use memory_open(id) to read the full memory before relying on it. Ignore hints that are clearly unrelated or too low-value for the task.
|
|
102
102
|
- Use memory_search(query) for full-text search across all memories.
|
|
@@ -32,24 +32,7 @@ export function normalizeTags(tags) {
|
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function expandTags(tags) {
|
|
36
|
-
const terms = [];
|
|
37
|
-
for (const tag of tags) {
|
|
38
|
-
terms.push(tag);
|
|
39
|
-
for (const part of tag.split(/[\/_-]+/)) {
|
|
40
|
-
if (part) terms.push(part);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return [...new Set(terms.map(normalizeText).filter(Boolean))];
|
|
44
|
-
}
|
|
45
35
|
|
|
46
|
-
export function quoteFtsTerm(term) {
|
|
47
|
-
return `"${String(term).replace(/"/g, '""')}"`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function normalizeText(text) {
|
|
51
|
-
return String(text ?? "").trim().toLowerCase();
|
|
52
|
-
}
|
|
53
36
|
|
|
54
37
|
export function generateMemoryId() {
|
|
55
38
|
return `mem_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
@@ -1,28 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function formatRecallHints(source, hints = []) {
|
|
1
|
+
export function formatRecallHints(hints = []) {
|
|
4
2
|
if (!hints.length) return "";
|
|
5
|
-
const lines = [
|
|
3
|
+
const lines = ["[recall]"];
|
|
6
4
|
for (const hint of hints) {
|
|
7
|
-
lines.push(`- ${hint.id} | ${hint.name} | ${hint.description}`);
|
|
5
|
+
lines.push(`- ${hint.id}${formatScoreForPrompt(hint.score)} | ${hint.name} | ${hint.description}`);
|
|
8
6
|
}
|
|
9
7
|
return lines.join("\n");
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
if (currentProject) {
|
|
20
|
-
const projectTag = normalizeText(`project/${currentProject}`);
|
|
21
|
-
if (entry.tags.map(normalizeText).includes(projectTag)) score += 2;
|
|
22
|
-
}
|
|
23
|
-
return score;
|
|
10
|
+
export function toHint(entry, metadata = {}) {
|
|
11
|
+
return { id: entry.id, name: entry.name, description: entry.description, ...metadata };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatScoreForPrompt(score) {
|
|
15
|
+
return Number.isFinite(score) ? ` | score=${formatScore(score)}` : "";
|
|
24
16
|
}
|
|
25
17
|
|
|
26
|
-
export function
|
|
27
|
-
return
|
|
18
|
+
export function formatScore(score) {
|
|
19
|
+
return Number.isFinite(score) ? score.toFixed(2) : "--";
|
|
28
20
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function preloadSemanticMemoryRecall({ memoryStore, ui = null, logger = null } = {}) {
|
|
2
|
+
if (!memoryStore?.semanticRecall?.enabled) return { ok: true, skipped: true };
|
|
3
|
+
try {
|
|
4
|
+
ui?.status?.("Preparing memory recall model...");
|
|
5
|
+
await memoryStore.semanticRecall.preload();
|
|
6
|
+
memoryStore.semanticRecallWarning = memoryStore.semanticRecall.warning;
|
|
7
|
+
if (memoryStore.semanticRecallWarning) ui?.writeln?.(`Memory recall fallback: ${memoryStore.semanticRecallWarning}`);
|
|
8
|
+
logger?.event?.("memory.semantic_model_ready", { modelId: memoryStore.semanticRecall.modelId, status: memoryStore.semanticRecall.status });
|
|
9
|
+
return { ok: true, skipped: false, fallback: memoryStore.semanticRecall.status === "fallback" };
|
|
10
|
+
} catch (err) {
|
|
11
|
+
const message = err?.message ?? String(err);
|
|
12
|
+
memoryStore.semanticRecallWarning = message;
|
|
13
|
+
logger?.error?.("memory.semantic_model_preload_failed", { error: message });
|
|
14
|
+
ui?.writeln?.(`Memory recall model preload failed: ${message}`);
|
|
15
|
+
return { ok: false, error: message };
|
|
16
|
+
}
|
|
17
|
+
}
|