march-cli 0.1.45 → 0.1.46

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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/cache.mjs +13 -5
  3. package/src/agent/code-search/engine.mjs +7 -2
  4. package/src/agent/code-search/retrieval/safetensors.mjs +16 -10
  5. package/src/agent/code-search/scanner.mjs +11 -5
  6. package/src/agent/model-payload-dumper.mjs +1 -1
  7. package/src/agent/runner/payload/provider-payload-transform.mjs +59 -0
  8. package/src/agent/runner/recall/mid-turn-recall-bridge.mjs +23 -0
  9. package/src/agent/runner.mjs +28 -27
  10. package/src/agent/runtime/remote-ui-client.mjs +1 -1
  11. package/src/agent/runtime/resource/context-resource-loader.mjs +17 -0
  12. package/src/agent/runtime/runtime-factory.mjs +5 -1
  13. package/src/agent/runtime/state/runner-state.mjs +10 -3
  14. package/src/agent/runtime/ui-event-bridge.mjs +2 -2
  15. package/src/agent/turn/turn-runner.mjs +20 -16
  16. package/src/cli/fallback-ui.mjs +2 -2
  17. package/src/cli/repl-loop.mjs +5 -4
  18. package/src/cli/startup/app-runtime.mjs +2 -3
  19. package/src/cli/tui/input/mouse-selection-controller.mjs +19 -0
  20. package/src/cli/tui/output/selectable-copy.mjs +3 -3
  21. package/src/cli/tui/output-buffer.mjs +18 -0
  22. package/src/cli/tui/recall-rendering.mjs +30 -8
  23. package/src/cli/tui/selection/ansi-range.mjs +88 -0
  24. package/src/cli/tui/selection-screen.mjs +31 -99
  25. package/src/cli/turn/turn-input-preparer.mjs +9 -2
  26. package/src/cli/ui.mjs +2 -2
  27. package/src/context/engine.mjs +13 -3
  28. package/src/memory/markdown/semantic-preload.mjs +9 -0
  29. package/src/memory/markdown/semantic-recall.mjs +10 -6
  30. package/src/memory/markdown-store.mjs +19 -11
  31. package/src/web-ui/dist/assets/{index-CcbYCcWs.css → index-BG1Pxf1k.css} +1 -1
  32. package/src/web-ui/dist/assets/{index-CBYbNVgs.js → index-C0xOHlDz.js} +1 -1
  33. package/src/web-ui/dist/index.html +2 -2
  34. package/src/web-ui/runtime-host.mjs +15 -3
  35. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +9 -6
  36. package/src/web-ui/src/model.ts +2 -2
  37. package/src/web-ui/src/runtime/client.ts +1 -1
  38. package/src/web-ui/src/runtime/runtimeTimeline.ts +2 -2
  39. package/src/web-ui/src/styles/shell.css +1 -0
  40. package/src/web-ui/src/timelineAdapter.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -1,12 +1,13 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import { chunkFile } from "./chunker.mjs";
4
+ import { readCodeFileContent } from "./scanner.mjs";
4
5
  import { Bm25Index } from "./retrieval/bm25.mjs";
5
6
  import { describeVectorizer } from "./retrieval/resilient-vectorizer.mjs";
6
7
  import { LocalVectorIndex, defaultVectorizer } from "./retrieval/vector.mjs";
7
8
 
8
9
  const DEFAULT_MAX_FILE_ENTRIES = 8_000;
9
- const DEFAULT_MAX_INDEX_ENTRIES = 24;
10
+ const DEFAULT_MAX_INDEX_ENTRIES = 6;
10
11
 
11
12
  export class CodeSearchIndexCache {
12
13
  constructor({
@@ -25,7 +26,7 @@ export class CodeSearchIndexCache {
25
26
  this.dirty = false;
26
27
  }
27
28
 
28
- async build(files) {
29
+ async build(files, { includeVector = true } = {}) {
29
30
  await this.load();
30
31
  const chunks = [];
31
32
  let reusedFiles = 0;
@@ -39,7 +40,9 @@ export class CodeSearchIndexCache {
39
40
  reusedFiles += 1;
40
41
  continue;
41
42
  }
42
- const fileChunks = await chunkFile(file);
43
+ const content = await readCodeFileContent(file.absPath);
44
+ if (content === null) continue;
45
+ const fileChunks = await chunkFile({ ...file, content });
43
46
  this.fileChunks.set(key, { signature, chunks: fileChunks });
44
47
  this.dirty = true;
45
48
  chunks.push(...fileChunks);
@@ -49,7 +52,7 @@ export class CodeSearchIndexCache {
49
52
  this.pruneFileCache();
50
53
  await this.persist();
51
54
 
52
- const indexSignature = [this.vectorizer.id, ...files.map(fileSignature)].join("\n");
55
+ const indexSignature = this.indexSignature(files, { includeVector });
53
56
  const cachedIndex = this.indices.get(indexSignature);
54
57
  if (cachedIndex) {
55
58
  this.indices.delete(indexSignature);
@@ -59,13 +62,18 @@ export class CodeSearchIndexCache {
59
62
 
60
63
  const index = {
61
64
  lexical: new Bm25Index(chunks),
62
- vector: await LocalVectorIndex.create(chunks, { vectorizer: this.vectorizer }),
65
+ vector: includeVector ? await LocalVectorIndex.create(chunks, { vectorizer: this.vectorizer }) : null,
63
66
  };
64
67
  this.indices.set(indexSignature, index);
65
68
  this.pruneIndexCache();
66
69
  return { chunks, index, reusedFiles, indexedFiles, reusedIndex: false, ...describeVectorizer(this.vectorizer) };
67
70
  }
68
71
 
72
+ indexSignature(files, { includeVector }) {
73
+ const vectorKey = includeVector ? this.vectorizer.id : "lexical";
74
+ return [vectorKey, ...files.map(fileSignature)].join("\n");
75
+ }
76
+
69
77
  clear() {
70
78
  this.fileChunks.clear();
71
79
  this.indices.clear();
@@ -22,7 +22,7 @@ export async function searchCode(options = {}) {
22
22
  if (!normalizedQuery && !related_to) return { results: [], stats: { files: 0, chunks: 0 } };
23
23
 
24
24
  const files = await scanCodeFiles({ root, path });
25
- const built = await activeCache.build(files);
25
+ const built = await activeCache.build(files, { includeVector: needsVectorIndex({ mode, related_to }) });
26
26
  const related = related_to ? relatedQuery(built.chunks, related_to, normalizedQuery) : null;
27
27
  const queryText = related?.query ?? normalizedQuery;
28
28
  const retrieved = await retrieveChunks(built.index, queryText, mode);
@@ -38,7 +38,7 @@ export async function searchCode(options = {}) {
38
38
  async function retrieveChunks(index, queryText, mode) {
39
39
  const lexical = index.lexical.search(queryText, { limit: RETRIEVAL_LIMIT });
40
40
  if (mode === "lexical" || mode === "symbol") return lexical;
41
- const semantic = await index.vector.search(queryText, { limit: RETRIEVAL_LIMIT });
41
+ const semantic = index.vector ? await index.vector.search(queryText, { limit: RETRIEVAL_LIMIT }) : [];
42
42
  if (mode === "semantic") return semantic;
43
43
  return rrfFuse([
44
44
  { results: lexical, weight: 1.2 },
@@ -46,6 +46,11 @@ async function retrieveChunks(index, queryText, mode) {
46
46
  ], { limit: RETRIEVAL_LIMIT });
47
47
  }
48
48
 
49
+ function needsVectorIndex({ mode, related_to }) {
50
+ if (related_to) return true;
51
+ return mode !== "lexical" && mode !== "symbol";
52
+ }
53
+
49
54
  function resultMode({ related_to, mode }) {
50
55
  if (related_to) return "related";
51
56
  if (mode === "symbol") return "symbol";
@@ -16,34 +16,40 @@ export async function readSafetensors(filePath) {
16
16
  }
17
17
 
18
18
  export function parseSafetensors(buffer) {
19
- const data = toArrayBuffer(buffer);
20
- const headerLength = Number(new DataView(data, 0, HEADER_BYTES).getBigUint64(0, true));
19
+ const source = toByteSource(buffer);
20
+ const headerLength = Number(new DataView(source.buffer, source.byteOffset, HEADER_BYTES).getBigUint64(0, true));
21
21
  const headerStart = HEADER_BYTES;
22
22
  const headerEnd = headerStart + headerLength;
23
- const headerJson = new TextDecoder().decode(new Uint8Array(data, headerStart, headerLength));
23
+ const headerJson = new TextDecoder().decode(new Uint8Array(source.buffer, source.byteOffset + headerStart, headerLength));
24
24
  const header = JSON.parse(headerJson);
25
25
  return {
26
26
  names: Object.keys(header).filter((name) => name !== "__metadata__"),
27
27
  getTensor(name) {
28
28
  const descriptor = header[name];
29
29
  if (!descriptor) throw new Error(`Missing safetensors tensor: ${name}`);
30
- return readTensor(data, headerEnd, descriptor);
30
+ return readTensor(source, headerEnd, descriptor);
31
31
  },
32
32
  };
33
33
  }
34
34
 
35
- function readTensor(data, dataStart, descriptor) {
35
+ function readTensor(source, dataStart, descriptor) {
36
36
  const reader = DTYPE_READERS[descriptor.dtype];
37
37
  if (!reader) throw new Error(`Unsupported safetensors dtype: ${descriptor.dtype}`);
38
38
  const [start, end] = descriptor.data_offsets;
39
- const byteOffset = dataStart + start;
40
- const length = (end - start) / reader.size;
41
- const view = new DataView(data, byteOffset, end - start);
39
+ const byteOffset = source.byteOffset + dataStart + start;
40
+ const byteLength = end - start;
41
+ const length = byteLength / reader.size;
42
+ if (descriptor.dtype === "F32" && byteOffset % Float32Array.BYTES_PER_ELEMENT === 0) {
43
+ return { values: new Float32Array(source.buffer, byteOffset, length), shape: descriptor.shape, dtype: descriptor.dtype };
44
+ }
45
+ const view = new DataView(source.buffer, byteOffset, byteLength);
42
46
  const values = new Float32Array(length);
43
47
  for (let index = 0; index < length; index += 1) values[index] = reader.read(view, index * reader.size);
44
48
  return { values, shape: descriptor.shape, dtype: descriptor.dtype };
45
49
  }
46
50
 
47
- function toArrayBuffer(buffer) {
48
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
51
+ function toByteSource(buffer) {
52
+ if (buffer instanceof ArrayBuffer) return { buffer, byteOffset: 0, byteLength: buffer.byteLength };
53
+ if (ArrayBuffer.isView(buffer)) return { buffer: buffer.buffer, byteOffset: buffer.byteOffset, byteLength: buffer.byteLength };
54
+ throw new TypeError("safetensors input must be an ArrayBuffer or typed array view");
49
55
  }
@@ -26,13 +26,11 @@ export async function scanCodeFiles({ root, path = ".", maxFiles = DEFAULT_MAX_F
26
26
  let info;
27
27
  try { info = await stat(absPath); } catch { continue; }
28
28
  if (!info.isFile() || info.size > maxFileBytes) continue;
29
- const content = await readUtf8Text(absPath);
30
- if (content === null) continue;
29
+ if (await isSymlink(absPath)) continue;
31
30
  files.push({
32
31
  absPath,
33
32
  relPath: relPath.replace(/\\/g, "/"),
34
33
  language: languageForPath(relPath),
35
- content,
36
34
  size: info.size,
37
35
  mtimeMs: info.mtimeMs,
38
36
  });
@@ -40,6 +38,7 @@ export async function scanCodeFiles({ root, path = ".", maxFiles = DEFAULT_MAX_F
40
38
  return files;
41
39
  }
42
40
 
41
+
43
42
  async function listCandidateFiles(root, base) {
44
43
  const fromRipgrep = await listRipgrepFiles(root, base);
45
44
  if (fromRipgrep) return fromRipgrep;
@@ -72,9 +71,8 @@ async function listFilesRecursively(root, dir) {
72
71
  return files;
73
72
  }
74
73
 
75
- async function readUtf8Text(path) {
74
+ export async function readCodeFileContent(path) {
76
75
  try {
77
- if ((await lstat(path)).isSymbolicLink()) return null;
78
76
  const buffer = await readFile(path);
79
77
  if (buffer.includes(0)) return null;
80
78
  return buffer.toString("utf8");
@@ -82,3 +80,11 @@ async function readUtf8Text(path) {
82
80
  return null;
83
81
  }
84
82
  }
83
+
84
+ async function isSymlink(path) {
85
+ try {
86
+ return (await lstat(path)).isSymbolicLink();
87
+ } catch {
88
+ return true;
89
+ }
90
+ }
@@ -14,7 +14,7 @@ export function installModelPayloadDumper(session, modelContextDumper, getKind =
14
14
  const originalEffectivePayload = replacement === undefined ? payload : replacement;
15
15
  const kind = getKind();
16
16
  const effectivePayload = typeof transformPayload === "function"
17
- ? transformPayload(originalEffectivePayload, { kind, model })
17
+ ? await transformPayload(originalEffectivePayload, { kind, model })
18
18
  : originalEffectivePayload;
19
19
  onModelPayload?.({
20
20
  payload: effectivePayload,
@@ -0,0 +1,59 @@
1
+ import { injectHostedTools } from "../../../provider/hosted-tools.mjs";
2
+ import { replaceProviderContextMessages } from "../../model-payload-dumper.mjs";
3
+ import { applyCodexLargeContextGuardToPayload } from "../codex-large-context-guard.mjs";
4
+
5
+ export function createRunnerProviderPayloadTransform({
6
+ engine,
7
+ sessionBinding,
8
+ hostedTools,
9
+ getCurrentPrompt,
10
+ getContextMode,
11
+ getFastEntry,
12
+ waitForMidTurnRecall = null,
13
+ getMidTurnRecallMessages = null,
14
+ }) {
15
+ let didReplaceProviderContext = false;
16
+
17
+ return {
18
+ resetTurn() {
19
+ didReplaceProviderContext = false;
20
+ },
21
+ async transform(payload, { kind, model } = {}) {
22
+ if (kind !== "user") return payload;
23
+ const shouldReplaceProviderContext = getContextMode() !== "continueExistingPiTranscript"
24
+ && !didReplaceProviderContext;
25
+ let nextPayload = payload;
26
+ if (shouldReplaceProviderContext) {
27
+ nextPayload = replaceProviderContextMessages(payload, engine.buildProviderContext(getCurrentPrompt()));
28
+ didReplaceProviderContext = true;
29
+ } else {
30
+ await waitForMidTurnRecall?.();
31
+ nextPayload = appendMissingMidTurnRecallMessages(nextPayload, getMidTurnRecallMessages?.() ?? []);
32
+ }
33
+ nextPayload = injectHostedTools(nextPayload, model, hostedTools);
34
+ nextPayload = applyCodexLargeContextGuardToPayload(nextPayload, { model, session: sessionBinding.get() });
35
+ if (getFastEntry()) nextPayload = { ...nextPayload, service_tier: "priority" };
36
+ return nextPayload;
37
+ },
38
+ };
39
+ }
40
+
41
+ function appendMissingMidTurnRecallMessages(payload, recallMessages) {
42
+ if (!Array.isArray(payload?.messages) || recallMessages.length === 0) return payload;
43
+ const existingText = payload.messages.map((message) => providerMessageText(message)).join("\n");
44
+ const missing = recallMessages.filter((content) => content && !existingText.includes(content));
45
+ if (missing.length === 0) return payload;
46
+ return {
47
+ ...payload,
48
+ messages: [
49
+ ...payload.messages,
50
+ ...missing.map((content) => ({ role: "user", content })),
51
+ ],
52
+ };
53
+ }
54
+
55
+ function providerMessageText(message) {
56
+ if (typeof message?.content === "string") return message.content;
57
+ if (Array.isArray(message?.content)) return message.content.map((part) => part?.text ?? "").join("");
58
+ return "";
59
+ }
@@ -0,0 +1,23 @@
1
+ export function createMidTurnRecallBridge() {
2
+ const messages = [];
3
+ const tasks = new Set();
4
+ return {
5
+ reset() {
6
+ messages.length = 0;
7
+ tasks.clear();
8
+ },
9
+ track({ content, task } = {}) {
10
+ if (content && !messages.includes(content)) messages.push(content);
11
+ if (!task?.finally) return;
12
+ tasks.add(task);
13
+ task.finally(() => tasks.delete(task));
14
+ },
15
+ async wait() {
16
+ if (tasks.size === 0) return;
17
+ await Promise.allSettled([...tasks]);
18
+ },
19
+ messages() {
20
+ return [...messages];
21
+ },
22
+ };
23
+ }
@@ -5,15 +5,17 @@ import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs
5
5
  import { syncMarchSessionState } from "../session/state/march-session-sync.mjs";
6
6
  import { LspService } from "../lsp/service.mjs";
7
7
  import { formatLspServiceEvent } from "../lsp/status-message.mjs";
8
- import { estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
8
+ import { estimateProviderPayloadTokens, installModelPayloadDumper } from "./model-payload-dumper.mjs";
9
9
  import { resolveInitialModel, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
10
10
  import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
11
11
  import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
12
+ import { createMarchPiResourceLoader } from "./runtime/resource/context-resource-loader.mjs";
12
13
  import { createRuntimeUiBridge } from "./runtime/ui-event-bridge.mjs";
13
14
  import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
14
15
  import { buildNotificationActivation, installRunnerProcessGuards, notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
15
16
  import { dumpCodexTransportDebug, getCodexTransportDebugSnapshot } from "./runner/codex-transport-debug.mjs";
16
- import { applyCodexLargeContextGuardToPayload } from "./runner/codex-large-context-guard.mjs";
17
+ import { createRunnerProviderPayloadTransform } from "./runner/payload/provider-payload-transform.mjs";
18
+ import { createMidTurnRecallBridge } from "./runner/recall/mid-turn-recall-bridge.mjs";
17
19
  import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
18
20
  import { createSessionBinding } from "./session/session-binding.mjs";
19
21
  import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
@@ -23,13 +25,11 @@ import { beginLoggedTurn } from "./turn/turn-logging.mjs";
23
25
  import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastProvider } from "./runner/fast-model.mjs";
24
26
  import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
25
27
  import { registerCustomProviders } from "../provider/custom-provider.mjs";
26
- import { injectHostedTools } from "../provider/hosted-tools.mjs";
27
28
  import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
28
29
  import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
29
30
  import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/runner.mjs";
30
31
  export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
31
- export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
32
- export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
32
+ export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs"; export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
33
33
  export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncMarchSessionState: syncMarchSessionStateEnabled = false, syncPiSidecar = syncMarchSessionStateEnabled, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {}, notificationContext = null }) {
34
34
  installRunnerProcessGuards();
35
35
  if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
@@ -43,10 +43,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
43
43
  if (!selectedModel) throw new Error("No authenticated models available. Run: march provider --config");
44
44
  provider = selectedModel.provider;
45
45
  modelId = selectedModel.id;
46
- const settingsManager = SettingsManager.inMemory({
47
- compaction: { enabled: false },
48
- retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
49
- });
46
+ const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000, provider: { timeoutMs: 20000, maxRetries: 3, maxRetryDelayMs: 60000 } } });
50
47
  const { ui: runtimeUi, eventBus: runtimeUiEvents, detach: detachRuntimeUi } = createRuntimeUiBridge(ui);
51
48
  const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)), onStatusChange: (event) => onLspStatusChange?.(event) });
52
49
  const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, profilePaths, remoteMemorySources, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
@@ -54,11 +51,18 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
54
51
  const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
55
52
  const sessionBinding = createSessionBinding(null);
56
53
  let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
54
+ const midTurnRecallBridge = createMidTurnRecallBridge();
57
55
  const lifecycle = createRunnerLifecycle();
58
- let currentTurnContextMode = "rebuild";
59
- let nextTurnContextMode = "rebuild";
60
- let lastNotificationResult = null, runtimeHost = null, lifecycleAdapter = null;
61
- let _currentFastEntry = null;
56
+ let currentTurnContextMode = "rebuild", nextTurnContextMode = "rebuild";
57
+ let lastNotificationResult = null, runtimeHost = null, lifecycleAdapter = null, _currentFastEntry = null;
58
+ const providerPayloadTransform = createRunnerProviderPayloadTransform({
59
+ engine, sessionBinding, hostedTools,
60
+ getCurrentPrompt: () => currentPromptForContext,
61
+ getContextMode: () => currentTurnContextMode,
62
+ getFastEntry: () => _currentFastEntry,
63
+ waitForMidTurnRecall: () => midTurnRecallBridge.wait(),
64
+ getMidTurnRecallMessages: () => midTurnRecallBridge.messages(),
65
+ });
62
66
  if (useRuntimeHost) {
63
67
  runtimeHost = await createRunnerRuntimeHost({
64
68
  cwd, stateRoot, provider, modelId,
@@ -69,7 +73,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
69
73
  memoryTools, memoryStore, historyStore, shellRuntime, lspService, mcpTools, webTools,
70
74
  lifecycle, extensionPaths, hostedTools,
71
75
  onRebind: (session) => {
72
- installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
76
+ installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, providerPayloadTransform.transform);
73
77
  syncEngineSessionState(engine, session);
74
78
  },
75
79
  createAgentSessionRuntimeImpl,
@@ -83,13 +87,18 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
83
87
  authStorage: resolvedAuth, projectMarchDir,
84
88
  getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
85
89
  });
90
+ const resourceLoader = await createMarchPiResourceLoader({
91
+ cwd,
92
+ agentDir: stateRoot,
93
+ settingsManager,
94
+ });
86
95
  const { session } = await createAgentSessionImpl({
87
96
  cwd, agentDir: stateRoot, ...sessionOptions,
88
97
  authStorage: resolvedAuth, modelRegistry,
89
- sessionManager: resolvedSessionManager, settingsManager,
98
+ sessionManager: resolvedSessionManager, settingsManager, resourceLoader,
90
99
  });
91
100
  sessionBinding.set(session);
92
- installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
101
+ installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, providerPayloadTransform.transform);
93
102
  }
94
103
  syncEngineSessionState(engine, sessionBinding.get());
95
104
  lifecycleAdapter = createMarchLifecycleAdapter({
@@ -113,6 +122,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
113
122
  currentPromptForContext = prompt;
114
123
  const contextMode = nextTurnContextMode;
115
124
  currentTurnContextMode = contextMode;
125
+ providerPayloadTransform.resetTurn();
126
+ midTurnRecallBridge.reset();
116
127
  nextTurnContextMode = "rebuild";
117
128
  lifecycle.clearPendingAction();
118
129
  const turnStartedAt = Date.now();
@@ -129,6 +140,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
129
140
  autoNameSession,
130
141
  contextMode,
131
142
  recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
143
+ trackMidTurnRecallInjection: (injection) => midTurnRecallBridge.track(injection),
132
144
  });
133
145
  notifyTurnEndDetached(turnNotifier, {
134
146
  status: "success",
@@ -256,7 +268,6 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
256
268
  ]);
257
269
  },
258
270
  };
259
- return runner;
260
271
  function syncCurrentMarchSessionState() {
261
272
  return syncMarchSessionState({
262
273
  enabled: syncPiSidecar || syncMarchSessionStateEnabled, projectMarchDir, engine,
@@ -285,14 +296,4 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
285
296
  });
286
297
  onModelPayload?.(event);
287
298
  }
288
- function injectMarchSystemContext(payload, { kind, model } = {}) {
289
- if (kind !== "user") return payload;
290
- let nextPayload = currentTurnContextMode === "continueExistingPiTranscript"
291
- ? payload
292
- : replaceProviderContextMessages(payload, engine.buildProviderContext(currentPromptForContext));
293
- nextPayload = injectHostedTools(nextPayload, model, hostedTools);
294
- nextPayload = applyCodexLargeContextGuardToPayload(nextPayload, { model, session: sessionBinding.get() });
295
- if (_currentFastEntry) nextPayload = { ...nextPayload, service_tier: "priority" };
296
- return nextPayload;
297
- }
298
299
  }
@@ -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, report }) => peer.notify("uiEvent", { type: "recall", source, hints, report }),
16
+ recall: ({ hints, report, variant }) => peer.notify("uiEvent", { type: "recall", hints, report, variant }),
17
17
  editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
18
18
  };
19
19
  }
@@ -0,0 +1,17 @@
1
+ import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const MARCH_PI_RESOURCE_LOADER_OPTIONS = Object.freeze({
4
+ noContextFiles: true,
5
+ });
6
+
7
+ export async function createMarchPiResourceLoader({ cwd, agentDir, settingsManager, extraOptions = {} }) {
8
+ const resourceLoader = new DefaultResourceLoader({
9
+ ...MARCH_PI_RESOURCE_LOADER_OPTIONS,
10
+ ...extraOptions,
11
+ cwd,
12
+ agentDir,
13
+ settingsManager,
14
+ });
15
+ await resourceLoader.reload();
16
+ return resourceLoader;
17
+ }
@@ -2,6 +2,7 @@ import {
2
2
  createAgentSessionFromServices,
3
3
  createAgentSessionServices,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+ import { MARCH_PI_RESOURCE_LOADER_OPTIONS } from "./resource/context-resource-loader.mjs";
5
6
 
6
7
  export function createMarchRuntimeFactory({
7
8
  agentDir,
@@ -24,7 +25,10 @@ export function createMarchRuntimeFactory({
24
25
  authStorage,
25
26
  settingsManager,
26
27
  modelRegistry,
27
- resourceLoaderOptions,
28
+ resourceLoaderOptions: {
29
+ ...MARCH_PI_RESOURCE_LOADER_OPTIONS,
30
+ ...resourceLoaderOptions,
31
+ },
28
32
  });
29
33
  const sessionOptions = await resolveSessionOptions({ cwd, services });
30
34
  const result = await createFromServices({
@@ -13,6 +13,7 @@ export function createRunnerStateSnapshot(runner) {
13
13
  remoteMemorySources: engine.remoteMemorySources ?? [],
14
14
  turns: engine.turns ?? [],
15
15
  pendingAssistantRecallHints: engine.peekPendingAssistantRecallHints?.() ?? engine.pendingAssistantRecallHints ?? [],
16
+ pendingAssistantRecallReport: engine.peekPendingAssistantRecallReport?.() ?? engine.pendingAssistantRecallReport ?? null,
16
17
  pendingAssistantRecallHintsRendered: engine.hasRenderedPendingAssistantRecallHints?.() ?? engine.pendingAssistantRecallHintsRendered ?? false,
17
18
  recentRecallMemoryIds: [...(engine.getRecentRecallMemoryIds?.() ?? [])],
18
19
  },
@@ -41,23 +42,29 @@ export function createRunnerEngineStateFacade({ getState, setState }) {
41
42
  peekPendingAssistantRecallHints() {
42
43
  return engineState(getState()).pendingAssistantRecallHints ?? [];
43
44
  },
45
+ peekPendingAssistantRecallReport() {
46
+ return engineState(getState()).pendingAssistantRecallReport ?? null;
47
+ },
44
48
  hasRenderedPendingAssistantRecallHints() {
45
49
  return Boolean(engineState(getState()).pendingAssistantRecallHintsRendered);
46
50
  },
47
51
  markPendingAssistantRecallHintsRendered() {
48
52
  updateEngineState(getState, setState, (engine) => {
49
- if ((engine.pendingAssistantRecallHints ?? []).length > 0) {
53
+ if ((engine.pendingAssistantRecallHints ?? []).length > 0 || engine.pendingAssistantRecallReport) {
50
54
  engine.pendingAssistantRecallHintsRendered = true;
51
55
  }
52
56
  });
53
57
  },
54
58
  takePendingAssistantRecallHints() {
55
- const hints = engineState(getState()).pendingAssistantRecallHints ?? [];
59
+ const state = engineState(getState());
60
+ const hints = state.pendingAssistantRecallHints ?? [];
61
+ const report = state.pendingAssistantRecallReport ?? null;
56
62
  updateEngineState(getState, setState, (engine) => {
57
63
  engine.pendingAssistantRecallHints = [];
64
+ engine.pendingAssistantRecallReport = null;
58
65
  engine.pendingAssistantRecallHintsRendered = false;
59
66
  });
60
- return hints;
67
+ return { hints, report };
61
68
  },
62
69
  getRecentRecallMemoryIds() {
63
70
  return engineState(getState()).recentRecallMemoryIds ?? [];
@@ -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, report }) => eventBus.emit({ type: "recall", source, hints, report }),
53
+ recall: ({ hints, report, variant }) => eventBus.emit({ type: "recall", hints, report, variant }),
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?.({ hints: event.hints, report: event.report });
74
+ case "recall": return ui.recall?.({ hints: event.hints, report: event.report, variant: event.variant });
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;
@@ -20,6 +20,7 @@ export async function runRunnerTurn({
20
20
  autoNameSession,
21
21
  contextMode = "rebuild",
22
22
  recordHistory = null,
23
+ trackMidTurnRecallInjection = null,
23
24
  }) {
24
25
  const {
25
26
  userRecallHints = [],
@@ -57,7 +58,8 @@ export async function runRunnerTurn({
57
58
  }
58
59
  handleRunnerSessionEvent(event, { ui, engine, state: turnState });
59
60
  if (event.type === "tool_execution_start") {
60
- const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints });
61
+ const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints, trackMidTurnRecallInjection });
62
+ trackMidTurnRecallInjection?.({ task });
61
63
  midTurnRecallTasks.push(task);
62
64
  }
63
65
  });
@@ -169,16 +171,16 @@ function throwIfAssistantEndedWithError(turnState) {
169
171
 
170
172
  function queueMidTurnRecallHints(session, hints, logger) {
171
173
  const content = formatRecallHints(hints);
172
- if (!content) return;
173
- const injected = session.sendCustomMessage?.({
174
+ if (!content) return null;
175
+ const task = Promise.resolve(session.sendCustomMessage?.({
174
176
  customType: "march.recall",
175
177
  content,
176
178
  display: false,
177
179
  details: { type: "recall" },
178
- }, { deliverAs: "steer" });
179
- void injected?.catch?.((err) => {
180
+ }, { deliverAs: "steer" })).catch((err) => {
180
181
  logger?.debug("memory.mid_turn_recall.inject_failed", { errorMessage: err?.message ?? String(err) });
181
182
  });
183
+ return { content, task };
182
184
  }
183
185
 
184
186
  function logSessionEvent(logger, event) {
@@ -207,9 +209,9 @@ function logSessionEvent(logger, event) {
207
209
 
208
210
  async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
209
211
  closeAssistantReply({ ui, state: turnState });
210
- const assistantRecallHints = await flushAssistantRecall({ memoryStore, engine, turnState });
211
- engine.setPendingAssistantRecallHints?.(assistantRecallHints);
212
- const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
212
+ const assistantRecall = await flushAssistantRecall({ memoryStore, engine, turnState });
213
+ engine.setPendingAssistantRecallHints?.(assistantRecall.hints, assistantRecall.report);
214
+ const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecall.hints]);
213
215
 
214
216
  const turn = engine.recordTurn({
215
217
  userMessage: userMessage ?? prompt.slice(0, 300),
@@ -225,22 +227,24 @@ async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore,
225
227
  }
226
228
 
227
229
  async function flushAssistantRecall({ memoryStore, engine, turnState }) {
228
- if (!memoryStore) return [];
230
+ if (!memoryStore) return { hints: [], report: null };
229
231
  const text = assistantRecallDeltaText(turnState);
230
232
  advanceAssistantRecallCursor(turnState);
231
- if (!text.trim()) return [];
233
+ if (!text.trim()) return { hints: [], report: null };
232
234
  return await memoryStore.recallForAssistant(text, {
233
235
  excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
234
236
  });
235
237
  }
236
238
 
237
- async function flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints }) {
239
+ async function flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints, trackMidTurnRecallInjection }) {
238
240
  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 });
241
+ const { hints, report } = await flushAssistantRecall({ memoryStore, engine, turnState });
242
+ if (hints.length > 0) {
243
+ midTurnRecallHints.push(...hints);
244
+ const injection = queueMidTurnRecallHints(activeSession, hints, logger);
245
+ if (injection) trackMidTurnRecallInjection?.(injection);
246
+ }
247
+ if (report) ui.recall?.({ hints, report, variant: "assistant" });
244
248
  } catch (err) {
245
249
  logger?.debug("memory.mid_turn_recall.failed", { errorMessage: err?.message ?? String(err) });
246
250
  }
@@ -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, report }) => {
112
+ recall: ({ hints, report, variant }) => {
113
113
  ensureNewline();
114
- for (const line of formatRecallLines(hints, report)) stdout.write(`${brightBlack(line)}\n`);
114
+ for (const line of formatRecallLines(hints, report, { variant })) stdout.write(`${brightBlack(line)}\n`);
115
115
  },
116
116
  clearOutput: () => {},
117
117
  restoreTranscript: () => {},