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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/cache.mjs +3 -2
  3. package/src/agent/code-search/engine.mjs +2 -0
  4. package/src/agent/code-search/retrieval/resilient-vectorizer.mjs +59 -0
  5. package/src/agent/code-search/tool.mjs +11 -5
  6. package/src/agent/runtime/remote-ui-client.mjs +1 -1
  7. package/src/agent/runtime/ui-event-bridge.mjs +2 -2
  8. package/src/agent/turn/turn-runner.mjs +23 -16
  9. package/src/cli/fallback-ui.mjs +2 -2
  10. package/src/cli/repl-loop.mjs +7 -7
  11. package/src/cli/startup/app-runtime.mjs +5 -2
  12. package/src/cli/tui/output/timeline-block-restore.mjs +1 -1
  13. package/src/cli/tui/recall-rendering.mjs +14 -7
  14. package/src/cli/turn/turn-input-preparer.mjs +6 -4
  15. package/src/cli/ui.mjs +2 -2
  16. package/src/cli/workspace/tui-timeline-projection.mjs +1 -1
  17. package/src/context/engine.mjs +2 -2
  18. package/src/context/system-core/base.md +1 -1
  19. package/src/memory/markdown/markdown-format.mjs +0 -17
  20. package/src/memory/markdown/markdown-recall.mjs +11 -19
  21. package/src/memory/markdown/semantic-preload.mjs +17 -0
  22. package/src/memory/markdown/semantic-recall.mjs +165 -0
  23. package/src/memory/markdown/sqlite-index.mjs +1 -13
  24. package/src/memory/markdown-store.mjs +24 -52
  25. package/src/web-ui/dist/assets/{index-DrlJis_D.js → index-CBYbNVgs.js} +1 -1
  26. package/src/web-ui/dist/assets/{index-BQtl1uQs.css → index-CcbYCcWs.css} +1 -1
  27. package/src/web-ui/dist/index.html +2 -2
  28. package/src/web-ui/runtime-host.mjs +5 -2
  29. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +24 -0
  30. package/src/web-ui/src/model.ts +18 -0
  31. package/src/web-ui/src/runtime/client.ts +2 -1
  32. package/src/web-ui/src/runtime/runtimeTimeline.ts +5 -0
  33. package/src/web-ui/src/styles/shell.css +6 -0
  34. package/src/web-ui/src/timelineAdapter.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.42",
3
+ "version": "0.1.45",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -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, vectorizer: this.vectorizer.id };
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, vectorizer: this.vectorizer.id };
66
+ return { chunks, index, reusedFiles, indexedFiles, reusedIndex: false, ...describeVectorizer(this.vectorizer) };
66
67
  }
67
68
 
68
69
  clear() {
@@ -79,6 +79,8 @@ function formatStats(files, built, mode) {
79
79
  indexed_files: built.indexedFiles,
80
80
  reused_index: built.reusedIndex,
81
81
  vectorizer: built.vectorizer,
82
+ vectorizer_status: built.vectorizer_status,
83
+ vectorizer_warning: built.vectorizer_warning,
82
84
  };
83
85
  }
84
86
 
@@ -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 Model2VecVectorizer({ modelDir }),
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 header = `--- code_search (${results.length} results, ${stats.files} files, ${stats.chunks} chunks, ${stats.mode}) ---`;
65
- if (results.length === 0) return `${header}\nNo matching code snippets found.`;
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?.({ source: event.source, hints: event.hints });
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 hints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
61
- if (hints.length > 0) {
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
- finalizeTurn({
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("assistant", hints);
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: { source: "assistant" },
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, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
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, currentProject });
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, currentProject }) {
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);
@@ -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: () => {},
@@ -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?.({ source: "user", hints: turnInput.userRecallHints });
22
- if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
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?.({ source: "user", hints: turnInput.userRecallHints });
191
- if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
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?.({ source: "assistant", hints });
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
- if (!hints.length) return [];
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
- ...hints.flatMap(formatHintLines),
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 lines = [` • ${title}`];
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("user", userRecallHints),
19
- formatRecallHints("assistant", carryoverRecallHints),
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);
@@ -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("user", turn.userRecallHints ?? []);
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("assistant", turn.assistantRecallHints ?? []);
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 source="..."] blocks in recent_chat are lightweight recall hints matched from prior thinking output. Treat them as possibly relevant pointers, not as complete facts.
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
- import { expandTags, normalizeText } from "./markdown-format.mjs";
2
-
3
- export function formatRecallHints(source, hints = []) {
1
+ export function formatRecallHints(hints = []) {
4
2
  if (!hints.length) return "";
5
- const lines = [`[recall source="${source}"]`];
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 scoreEntry(entry, terms, currentProject) {
13
- const expanded = expandTags(entry.tags);
14
- let score = 0;
15
- for (const term of terms) {
16
- if (entry.tags.map(normalizeText).includes(term)) score += 10;
17
- else if (expanded.includes(term)) score += 5;
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 toHint(entry) {
27
- return { id: entry.id, name: entry.name, description: entry.description };
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
+ }