march-cli 0.1.41 → 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/runner.mjs +2 -1
- 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-events.mjs +6 -0
- package/src/agent/turn/turn-runner.mjs +110 -23
- 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) {
|
package/src/agent/runner.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
|
|
|
18
18
|
import { createSessionBinding } from "./session/session-binding.mjs";
|
|
19
19
|
import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
|
|
20
20
|
import { MARCH_BASE_TOOL_NAMES } from "./tool-names.mjs";
|
|
21
|
-
import { runRunnerTurn } from "./turn/turn-runner.mjs";
|
|
21
|
+
import { MODEL_STREAM_IDLE_TIMEOUT_CODE, runRunnerTurn } from "./turn/turn-runner.mjs";
|
|
22
22
|
import { beginLoggedTurn } from "./turn/turn-logging.mjs";
|
|
23
23
|
import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastProvider } from "./runner/fast-model.mjs";
|
|
24
24
|
import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
|
|
@@ -142,6 +142,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
142
142
|
turnLog.endSuccess(result);
|
|
143
143
|
return result;
|
|
144
144
|
} catch (err) {
|
|
145
|
+
if (err?.code === MODEL_STREAM_IDLE_TIMEOUT_CODE) nextTurnContextMode = "continueExistingPiTranscript";
|
|
145
146
|
notifyTurnEndDetached(turnNotifier, {
|
|
146
147
|
status: "error",
|
|
147
148
|
sessionName: engine.sessionName,
|
|
@@ -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;
|
|
@@ -12,6 +12,8 @@ export function createTurnEventState() {
|
|
|
12
12
|
assistantContextParts: [],
|
|
13
13
|
activeToolContextPart: null,
|
|
14
14
|
toolCalls: [],
|
|
15
|
+
lastAssistantStopReason: null,
|
|
16
|
+
lastAssistantErrorMessage: null,
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -19,6 +21,10 @@ export function handleRunnerSessionEvent(event, { ui, engine, state }) {
|
|
|
19
21
|
if (event.type === "message_update" && event.assistantMessageEvent) {
|
|
20
22
|
handleAssistantMessageEvent(event.assistantMessageEvent, { ui, state });
|
|
21
23
|
}
|
|
24
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
25
|
+
state.lastAssistantStopReason = event.message.stopReason ?? null;
|
|
26
|
+
state.lastAssistantErrorMessage = event.message.errorMessage ?? null;
|
|
27
|
+
}
|
|
22
28
|
if (event.type === "tool_execution_start") {
|
|
23
29
|
closeAssistantReply({ ui, state });
|
|
24
30
|
appendToolStartContext(state, event.toolName, event.args);
|
|
@@ -2,6 +2,8 @@ import { formatRecallHints } from "../../memory/markdown-store.mjs";
|
|
|
2
2
|
import { resolveImageAttachmentReferences } from "../../session/attachment-references.mjs";
|
|
3
3
|
import { closeAssistantReply, compactAssistantContext, createTurnEventState, handleRunnerSessionEvent } from "./turn-events.mjs";
|
|
4
4
|
|
|
5
|
+
export const MODEL_STREAM_IDLE_TIMEOUT_CODE = "MODEL_STREAM_IDLE_TIMEOUT";
|
|
6
|
+
|
|
5
7
|
export async function runRunnerTurn({
|
|
6
8
|
prompt,
|
|
7
9
|
userMessage,
|
|
@@ -21,30 +23,42 @@ export async function runRunnerTurn({
|
|
|
21
23
|
}) {
|
|
22
24
|
const {
|
|
23
25
|
userRecallHints = [],
|
|
24
|
-
currentProject = "",
|
|
25
26
|
} = options;
|
|
26
27
|
const activeSession = sessionBinding.get();
|
|
27
28
|
const turnState = createTurnEventState();
|
|
28
29
|
const midTurnRecallHints = [];
|
|
30
|
+
const midTurnRecallTasks = [];
|
|
31
|
+
const idleWatchdog = createModelStreamIdleWatchdog({ session: activeSession, logger, setPhase });
|
|
29
32
|
ui.turnStart();
|
|
30
33
|
setPhase?.("subscribed");
|
|
31
34
|
logger?.event("turn.ui.start");
|
|
32
35
|
|
|
33
36
|
const unsubscribe = activeSession.subscribe((event) => {
|
|
34
37
|
logSessionEvent(logger, event);
|
|
35
|
-
if (event.type === "tool_execution_start")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (event.type === "
|
|
38
|
+
if (event.type === "tool_execution_start") {
|
|
39
|
+
idleWatchdog.pause();
|
|
40
|
+
setPhase?.(`tool_running:${event.toolName ?? "unknown"}`);
|
|
41
|
+
}
|
|
42
|
+
if (event.type === "tool_execution_end") {
|
|
43
|
+
setPhase?.("model_streaming");
|
|
44
|
+
idleWatchdog.arm("tool_execution_end");
|
|
45
|
+
}
|
|
46
|
+
if (event.type === "auto_retry_start") {
|
|
47
|
+
idleWatchdog.pause();
|
|
48
|
+
setPhase?.("retry_wait");
|
|
49
|
+
}
|
|
50
|
+
if (event.type === "auto_retry_end") {
|
|
51
|
+
setPhase?.("model_streaming");
|
|
52
|
+
idleWatchdog.arm("auto_retry_end");
|
|
53
|
+
}
|
|
54
|
+
if (event.type === "message_update") {
|
|
55
|
+
setPhase?.("model_streaming");
|
|
56
|
+
idleWatchdog.arm("message_update");
|
|
57
|
+
}
|
|
40
58
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
41
59
|
if (event.type === "tool_execution_start") {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
midTurnRecallHints.push(...hints);
|
|
45
|
-
queueMidTurnRecallHints(activeSession, hints, logger);
|
|
46
|
-
ui.recall?.({ source: "assistant", hints });
|
|
47
|
-
}
|
|
60
|
+
const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints });
|
|
61
|
+
midTurnRecallTasks.push(task);
|
|
48
62
|
}
|
|
49
63
|
});
|
|
50
64
|
|
|
@@ -59,21 +73,24 @@ export async function runRunnerTurn({
|
|
|
59
73
|
logger?.event("model.prompt.start", { contextMode });
|
|
60
74
|
try {
|
|
61
75
|
if (contextMode === "rebuild") resetPiMessageHistory(activeSession);
|
|
62
|
-
|
|
76
|
+
idleWatchdog.arm("model_request");
|
|
77
|
+
await idleWatchdog.watch(activeSession.prompt(
|
|
63
78
|
contextMode === "continueExistingPiTranscript" ? (userMessage ?? prompt) : prompt,
|
|
64
79
|
attachmentReferences.images.length > 0 ? { images: attachmentReferences.images } : undefined,
|
|
65
|
-
);
|
|
80
|
+
));
|
|
81
|
+
throwIfAssistantEndedWithError(turnState);
|
|
66
82
|
} finally {
|
|
83
|
+
idleWatchdog.clear();
|
|
67
84
|
setModelCallKind("model");
|
|
68
85
|
logger?.event("model.prompt.end");
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
setPhase?.("finalizing");
|
|
72
|
-
|
|
89
|
+
await Promise.allSettled(midTurnRecallTasks);
|
|
90
|
+
await finalizeTurn({
|
|
73
91
|
prompt,
|
|
74
92
|
userMessage,
|
|
75
93
|
userRecallHints,
|
|
76
|
-
currentProject,
|
|
77
94
|
memoryStore,
|
|
78
95
|
engine,
|
|
79
96
|
ui,
|
|
@@ -86,19 +103,78 @@ export async function runRunnerTurn({
|
|
|
86
103
|
return { draft: turnState.draft };
|
|
87
104
|
} finally {
|
|
88
105
|
logger?.event("turn.ui.end");
|
|
106
|
+
idleWatchdog.clear();
|
|
89
107
|
ui.turnEnd();
|
|
90
108
|
unsubscribe();
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
function createModelStreamIdleWatchdog({ session, logger, setPhase }) {
|
|
113
|
+
const timeoutMs = getModelStreamIdleTimeoutMs();
|
|
114
|
+
let timer = null;
|
|
115
|
+
let timedOut = false;
|
|
116
|
+
let rejectIdle = null;
|
|
117
|
+
const idlePromise = new Promise((_, reject) => {
|
|
118
|
+
rejectIdle = reject;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
arm(reason) {
|
|
123
|
+
if (timeoutMs <= 0 || timedOut) return;
|
|
124
|
+
clearTimer();
|
|
125
|
+
timer = setTimeout(() => {
|
|
126
|
+
timedOut = true;
|
|
127
|
+
setPhase?.("model_idle_timeout");
|
|
128
|
+
logger?.event("model.stream.idle_timeout", { timeoutMs, reason });
|
|
129
|
+
try { session.abortRetry?.(); } catch {}
|
|
130
|
+
try { session.abort?.(); } catch {}
|
|
131
|
+
rejectIdle(createModelStreamIdleTimeoutError(timeoutMs, reason));
|
|
132
|
+
}, timeoutMs);
|
|
133
|
+
},
|
|
134
|
+
pause: clearTimer,
|
|
135
|
+
clear: clearTimer,
|
|
136
|
+
async watch(promise) {
|
|
137
|
+
const guarded = Promise.resolve(promise);
|
|
138
|
+
guarded.catch(() => {});
|
|
139
|
+
return await Promise.race([guarded, idlePromise]);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function clearTimer() {
|
|
144
|
+
if (!timer) return;
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
timer = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createModelStreamIdleTimeoutError(timeoutMs, reason) {
|
|
151
|
+
const error = new Error(`Model stream idle timeout after ${timeoutMs}ms (${reason}); aborted the current turn`);
|
|
152
|
+
error.code = MODEL_STREAM_IDLE_TIMEOUT_CODE;
|
|
153
|
+
return error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getModelStreamIdleTimeoutMs() {
|
|
157
|
+
const raw = process.env.MARCH_MODEL_STREAM_IDLE_TIMEOUT_MS;
|
|
158
|
+
if (raw === "0" || raw === "false" || raw === "no") return 0;
|
|
159
|
+
const parsed = raw ? Number.parseInt(raw, 10) : 18000;
|
|
160
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 18000;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function throwIfAssistantEndedWithError(turnState) {
|
|
164
|
+
if (turnState.lastAssistantStopReason !== "error") return;
|
|
165
|
+
const error = new Error(turnState.lastAssistantErrorMessage || "Model provider returned an error");
|
|
166
|
+
error.code = "MODEL_PROVIDER_ERROR";
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
|
|
94
170
|
function queueMidTurnRecallHints(session, hints, logger) {
|
|
95
|
-
const content = formatRecallHints(
|
|
171
|
+
const content = formatRecallHints(hints);
|
|
96
172
|
if (!content) return;
|
|
97
173
|
const injected = session.sendCustomMessage?.({
|
|
98
174
|
customType: "march.recall",
|
|
99
175
|
content,
|
|
100
176
|
display: false,
|
|
101
|
-
details: {
|
|
177
|
+
details: { type: "recall" },
|
|
102
178
|
}, { deliverAs: "steer" });
|
|
103
179
|
void injected?.catch?.((err) => {
|
|
104
180
|
logger?.debug("memory.mid_turn_recall.inject_failed", { errorMessage: err?.message ?? String(err) });
|
|
@@ -129,9 +205,9 @@ function logSessionEvent(logger, event) {
|
|
|
129
205
|
});
|
|
130
206
|
}
|
|
131
207
|
|
|
132
|
-
function finalizeTurn({ prompt, userMessage, userRecallHints,
|
|
208
|
+
async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
|
|
133
209
|
closeAssistantReply({ ui, state: turnState });
|
|
134
|
-
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState
|
|
210
|
+
const assistantRecallHints = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
135
211
|
engine.setPendingAssistantRecallHints?.(assistantRecallHints);
|
|
136
212
|
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
|
|
137
213
|
|
|
@@ -148,17 +224,28 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
|
|
|
148
224
|
syncCurrentMarchSessionState();
|
|
149
225
|
}
|
|
150
226
|
|
|
151
|
-
function flushAssistantRecall({ memoryStore, engine, turnState
|
|
227
|
+
async function flushAssistantRecall({ memoryStore, engine, turnState }) {
|
|
152
228
|
if (!memoryStore) return [];
|
|
153
229
|
const text = assistantRecallDeltaText(turnState);
|
|
154
230
|
advanceAssistantRecallCursor(turnState);
|
|
155
231
|
if (!text.trim()) return [];
|
|
156
|
-
return memoryStore.recallForAssistant(text, {
|
|
157
|
-
currentProject,
|
|
232
|
+
return await memoryStore.recallForAssistant(text, {
|
|
158
233
|
excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
|
|
159
234
|
});
|
|
160
235
|
}
|
|
161
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
|
+
|
|
162
249
|
function assistantRecallDeltaText(turnState) {
|
|
163
250
|
const cursor = turnState.recallCursor ?? { draftLength: 0, thinkingLength: 0 };
|
|
164
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
|
}
|