march-cli 0.1.35 → 0.1.37

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 (63) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/cache.mjs +133 -0
  3. package/src/agent/code-search/chunk-rules.mjs +107 -0
  4. package/src/agent/code-search/chunker.mjs +125 -0
  5. package/src/agent/code-search/engine.mjs +109 -0
  6. package/src/agent/code-search/languages.mjs +25 -0
  7. package/src/agent/code-search/parser-pool.mjs +29 -0
  8. package/src/agent/code-search/rerank.mjs +43 -0
  9. package/src/agent/code-search/retrieval/bm25.mjs +47 -0
  10. package/src/agent/code-search/retrieval/fusion.mjs +18 -0
  11. package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
  12. package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
  13. package/src/agent/code-search/retrieval/vector.mjs +107 -0
  14. package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
  15. package/src/agent/code-search/scanner.mjs +84 -0
  16. package/src/agent/code-search/tokenize.mjs +16 -0
  17. package/src/agent/code-search/tool.mjs +75 -0
  18. package/src/agent/runner/provider-quota-runtime.mjs +38 -0
  19. package/src/agent/runner.mjs +14 -10
  20. package/src/agent/runtime/remote-runner-client.mjs +2 -0
  21. package/src/agent/runtime/runner-ipc-target.mjs +7 -0
  22. package/src/agent/runtime/runner-process-client.mjs +5 -0
  23. package/src/agent/runtime/runner-process-factory.mjs +5 -0
  24. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  25. package/src/agent/runtime/state/runner-state.mjs +1 -0
  26. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  27. package/src/agent/session/session-options.mjs +2 -1
  28. package/src/agent/tools.mjs +7 -1
  29. package/src/agent/turn/turn-events.mjs +41 -0
  30. package/src/agent/turn/turn-runner.mjs +5 -2
  31. package/src/cli/commands/registry/slash-command-registry.mjs +10 -7
  32. package/src/cli/commands/status-command.mjs +61 -35
  33. package/src/cli/input/history-store.mjs +65 -3
  34. package/src/cli/repl-loop.mjs +8 -6
  35. package/src/cli/startup/app-runtime.mjs +5 -29
  36. package/src/cli/startup/create-runtime-runner.mjs +4 -46
  37. package/src/cli/tui/input/history-navigation-controller.mjs +56 -0
  38. package/src/cli/turn/turn-input-preparer.mjs +0 -1
  39. package/src/cli/ui.mjs +9 -0
  40. package/src/context/engine.mjs +4 -2
  41. package/src/context/system-core/base.md +9 -1
  42. package/src/history/runner.mjs +11 -0
  43. package/src/history/store.mjs +129 -0
  44. package/src/history/tool.mjs +39 -0
  45. package/src/lsp/client.mjs +12 -5
  46. package/src/lsp/service.mjs +15 -3
  47. package/src/main.mjs +1 -2
  48. package/src/provider/quota/codex.mjs +278 -0
  49. package/src/provider/quota/index.mjs +46 -0
  50. package/src/provider/quota/transport-observer.mjs +99 -0
  51. package/src/web-ui/command.mjs +2 -2
  52. package/src/web-ui/runtime-host.mjs +7 -23
  53. package/src/web-ui/server.mjs +1 -0
  54. package/src/web-ui/session-manager.mjs +4 -2
  55. package/src/web-ui/src/components/AppShell.tsx +1 -0
  56. package/src/web-ui/src/components/RightSidebar.tsx +47 -2
  57. package/src/web-ui/src/model.ts +20 -0
  58. package/src/web-ui/src/runtime/client.ts +8 -1
  59. package/src/web-ui/src/runtime/useWebRuntime.ts +13 -1
  60. package/src/web-ui/src/styles/shell.css +10 -0
  61. package/src/web-ui/dist/assets/index-BUmhnID4.css +0 -1
  62. package/src/web-ui/dist/assets/index-CtuqTjcB.js +0 -1845
  63. package/src/web-ui/dist/index.html +0 -13
@@ -10,6 +10,7 @@ export async function createRunnerProcessClient({
10
10
  runnerOptions,
11
11
  ui,
12
12
  onModelPayload = null,
13
+ onLspStatusChange = null,
13
14
  entry = fileURLToPath(DEFAULT_ENTRY),
14
15
  forkImpl = fork,
15
16
  timeoutMs = 0,
@@ -46,6 +47,10 @@ export async function createRunnerProcessClient({
46
47
  target: {
47
48
  ...createRuntimeUiEventTarget(ui),
48
49
  modelPayload: (event) => onModelPayload?.(event),
50
+ lspStatusChange: async (event) => {
51
+ await active?.runner?.refreshState?.();
52
+ onLspStatusChange?.(event);
53
+ },
49
54
  },
50
55
  timeoutMs,
51
56
  });
@@ -93,6 +93,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
93
93
  }),
94
94
  logger,
95
95
  onModelPayload: (event) => d.peer.notify("modelPayload", pickModelPayloadEvent(event)),
96
+ onLspStatusChange: (event) => d.peer.notify("lspStatusChange", pickLspStatusEvent(event)),
96
97
  });
97
98
 
98
99
  const originalDispose = runner.dispose;
@@ -109,3 +110,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
109
110
  function pickModelPayloadEvent({ estimatedTokens, provider, model, kind, turnId } = {}) {
110
111
  return { estimatedTokens, provider, model, kind, turnId };
111
112
  }
113
+
114
+ function pickLspStatusEvent({ id, root, status, reason, managed } = {}) {
115
+ return { id, root, status, reason, managed };
116
+ }
@@ -20,6 +20,7 @@ export async function createRunnerRuntimeHost({
20
20
  ui,
21
21
  projectMarchDir = null,
22
22
  memoryTools = [],
23
+ historyStore = null,
23
24
  shellRuntime = null,
24
25
  lspService = null,
25
26
  mcpTools = [],
@@ -56,6 +57,7 @@ export async function createRunnerRuntimeHost({
56
57
  engine,
57
58
  ui,
58
59
  memoryTools,
60
+ historyStore,
59
61
  shellRuntime,
60
62
  lspService,
61
63
  mcpTools,
@@ -22,6 +22,7 @@ export function createRunnerStateSnapshot(runner) {
22
22
  availableThinkingLevels: runner.getAvailableThinkingLevels?.() ?? [],
23
23
  canSwitchPiSession: runner.canSwitchPiSession?.() ?? false,
24
24
  sessionStats: runner.getSessionStats?.() ?? null,
25
+ providerQuota: runner.getCachedProviderQuotaSnapshot?.() ?? null,
25
26
  lspStatus: runner.getLspStatus?.() ?? null,
26
27
  extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
27
28
  extensionLifecycleState: runner.getExtensionLifecycleState?.() ?? null,
@@ -51,6 +51,7 @@ export function createRuntimeUiClient(eventBus) {
51
51
  status: (text) => eventBus.emit({ type: "status", text }),
52
52
  debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
53
53
  recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
54
+ providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
54
55
  editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
55
56
  requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
56
57
  };
@@ -72,6 +73,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
72
73
  case "status": return ui.status?.(event.text);
73
74
  case "debug_lines": return writeDebugLines(ui, event.lines);
74
75
  case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
76
+ case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
75
77
  case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
76
78
  case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
77
79
  default: return undefined;
@@ -10,6 +10,7 @@ export function resolveRunnerSessionOptions({
10
10
  engine,
11
11
  ui,
12
12
  memoryTools = [],
13
+ historyStore = null,
13
14
  shellRuntime = null,
14
15
  lspService = null,
15
16
  mcpTools = [],
@@ -31,7 +32,7 @@ export function resolveRunnerSessionOptions({
31
32
  ?? (provider && modelId ? getModel(provider, modelId) : null);
32
33
  if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
33
34
 
34
- const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
35
+ const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
35
36
  const customToolNames = customTools.map((tool) => tool.name);
36
37
  const tools = [
37
38
  ...customToolNames.filter((name) => name === "read"),
@@ -1,4 +1,5 @@
1
1
  import { createCommandExecTool } from "./command-exec-tool.mjs";
2
+ import { createCodeSearchTool } from "./code-search/tool.mjs";
2
3
  import { createContextStatsTool } from "./context-stats-tool.mjs";
3
4
  import { createEditFileTool } from "./file-edit-tool.mjs";
4
5
  import { createReadFileTool } from "./file-tools/read-file-tool.mjs";
@@ -12,10 +13,13 @@ import { initImageGen } from "../image-gen/index.mjs";
12
13
  import { createSuperGrokTool } from "../supergrok/tool.mjs";
13
14
  import { createBrowserTools } from "../browser/tools/index.mjs";
14
15
  import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
16
+ import { createHistorySearchTool } from "../history/tool.mjs";
15
17
 
16
- export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
18
+ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], historyStore = null, shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
17
19
  const commandExecTool = createCommandExecTool({ cwd });
20
+ const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
18
21
  const contextStatsTool = createContextStatsTool({ engine });
22
+ const historySearchTool = createHistorySearchTool({ store: historyStore });
19
23
  const editFileTool = createEditFileTool({ engine, ui, lspService });
20
24
  const readFileTool = createReadFileTool({ engine });
21
25
  const readImageTool = createReadImageTool({ engine, getCurrentModel });
@@ -30,9 +34,11 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
30
34
  screenTool,
31
35
  listWindowsTool,
32
36
  contextStatsTool,
37
+ codeSearchTool,
33
38
  commandExecTool,
34
39
  editFileTool,
35
40
  ...createShellTools(shellRuntime),
41
+ ...(historySearchTool ? [historySearchTool] : []),
36
42
  ...memoryTools,
37
43
  ...mcpTools,
38
44
  ...webTools,
@@ -1,5 +1,7 @@
1
1
  import { formatToolStartLine, formatToolSuccessSummary } from "../tool-summary.mjs";
2
2
 
3
+ const TOOL_ERROR_EXCERPT_LIMIT = 4000;
4
+
3
5
  export function createTurnEventState() {
4
6
  return {
5
7
  draft: "",
@@ -9,6 +11,7 @@ export function createTurnEventState() {
9
11
  assistantReplyOpen: false,
10
12
  assistantContextParts: [],
11
13
  activeToolContextPart: null,
14
+ toolCalls: [],
12
15
  };
13
16
  }
14
17
 
@@ -19,10 +22,12 @@ export function handleRunnerSessionEvent(event, { ui, engine, state }) {
19
22
  if (event.type === "tool_execution_start") {
20
23
  closeAssistantReply({ ui, state });
21
24
  appendToolStartContext(state, event.toolName, event.args);
25
+ recordToolStart(state, event.toolName, event.args);
22
26
  ui.toolStart(event.toolName, event.args);
23
27
  }
24
28
  if (event.type === "tool_execution_end") {
25
29
  updateToolEndContext(state, event.toolName, event.isError, event.result);
30
+ recordToolEnd(state, event.toolName, event.isError, event.result);
26
31
  ui.toolEnd(event.toolName, event.isError, event.result);
27
32
  }
28
33
  if (event.type === "auto_retry_start") {
@@ -109,3 +114,39 @@ function updateToolEndContext(state, name, isError, result) {
109
114
  if (summary && summary !== "done") part.text = `${part.text.trimEnd()} (${summary})\n`;
110
115
  state.activeToolContextPart = null;
111
116
  }
117
+
118
+ function recordToolStart(state, name, args) {
119
+ state.toolCalls.push({ name, args: cloneJson(args), status: "running" });
120
+ }
121
+
122
+ function recordToolEnd(state, name, isError, result) {
123
+ const call = [...state.toolCalls].reverse().find((item) => item.name === name && item.status === "running");
124
+ if (!call) return;
125
+ call.status = isError ? "failed" : "success";
126
+ if (!isError) return;
127
+ const output = extractToolOutput(result);
128
+ call.error = {
129
+ message: output.split(/\r?\n/).find(Boolean) ?? "Tool call failed",
130
+ details: cloneJson(result?.details ?? null),
131
+ excerpt: truncate(output, TOOL_ERROR_EXCERPT_LIMIT),
132
+ };
133
+ }
134
+
135
+ function extractToolOutput(result) {
136
+ const content = result?.content;
137
+ if (!Array.isArray(content)) return typeof result === "string" ? result : "";
138
+ return content.filter((item) => item?.type === "text").map((item) => item.text ?? "").join("\n");
139
+ }
140
+
141
+ function cloneJson(value) {
142
+ try {
143
+ return JSON.parse(JSON.stringify(value ?? null));
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function truncate(text, limit) {
150
+ const value = String(text ?? "");
151
+ return value.length > limit ? `${value.slice(0, limit)}\n[truncated]` : value;
152
+ }
@@ -17,6 +17,7 @@ export async function runRunnerTurn({
17
17
  syncCurrentPiSidecar,
18
18
  autoNameSession,
19
19
  contextMode = "rebuild",
20
+ recordHistory = null,
20
21
  }) {
21
22
  const {
22
23
  userRecallHints = [],
@@ -80,6 +81,7 @@ export async function runRunnerTurn({
80
81
  midTurnRecallHints,
81
82
  syncCurrentPiSidecar,
82
83
  autoNameSession,
84
+ recordHistory,
83
85
  });
84
86
  return { draft: turnState.draft };
85
87
  } finally {
@@ -127,19 +129,20 @@ function logSessionEvent(logger, event) {
127
129
  });
128
130
  }
129
131
 
130
- function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
132
+ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession, recordHistory }) {
131
133
  closeAssistantReply({ ui, state: turnState });
132
134
  const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
133
135
  engine.setPendingAssistantRecallHints?.(assistantRecallHints);
134
136
  const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
135
137
 
136
- engine.recordTurn({
138
+ const turn = engine.recordTurn({
137
139
  userMessage: userMessage ?? prompt.slice(0, 300),
138
140
  assistantMessage: turnState.draft,
139
141
  assistantContext: compactAssistantContext(turnState),
140
142
  userRecallHints,
141
143
  assistantRecallHints: recordedAssistantRecallHints,
142
144
  });
145
+ recordHistory?.({ ...turn, thinking: assistantThinkingText(turnState), toolCalls: turnState.toolCalls });
143
146
 
144
147
  autoNameSession?.();
145
148
  syncCurrentPiSidecar();
@@ -97,13 +97,16 @@ export const SLASH_COMMANDS = [
97
97
  exactCommand({
98
98
  name: "status",
99
99
  description: "Show runtime status",
100
- run: async ({ ui, runner, sessionState, sessionSource }) => writeLines(ui, statusCommand({
101
- runner,
102
- sessionState,
103
- sessionSource,
104
- extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
105
- lifecycleState: runner.getExtensionLifecycleState?.() ?? null,
106
- })),
100
+ run: async ({ ui, runner, sessionState, sessionSource }) => {
101
+ await runner.getProviderQuotaSnapshot?.({ emit: true }).catch(() => null);
102
+ return writeLines(ui, statusCommand({
103
+ runner,
104
+ sessionState,
105
+ sessionSource,
106
+ extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
107
+ lifecycleState: runner.getExtensionLifecycleState?.() ?? null,
108
+ }));
109
+ },
107
110
  }),
108
111
  exactCommand({
109
112
  name: "notify",
@@ -10,15 +10,20 @@ export function statusCommand({
10
10
  lifecycleState = null,
11
11
  gitBranch = getGitBranch(runner.engine.cwd),
12
12
  }) {
13
- return [formatStatusLine({
14
- engine: runner.engine,
15
- sessionState,
16
- sessionStats: runner.getSessionStats?.() ?? null,
17
- sessionSource,
18
- extensionDiagnostics,
19
- lifecycleState,
20
- gitBranch,
21
- })];
13
+ const providerQuota = runner.getCachedProviderQuotaSnapshot?.() ?? null;
14
+ const lines = [
15
+ ...formatStatusLines({
16
+ engine: runner.engine,
17
+ sessionState,
18
+ sessionStats: runner.getSessionStats?.() ?? null,
19
+ sessionSource,
20
+ extensionDiagnostics,
21
+ lifecycleState,
22
+ gitBranch,
23
+ }),
24
+ ...formatProviderQuotaLines(providerQuota),
25
+ ];
26
+ return lines.length > 0 ? lines : ["No provider quota available."];
22
27
  }
23
28
 
24
29
  export function statusBarLine({
@@ -37,35 +42,48 @@ export function statusBarLine({
37
42
  });
38
43
  }
39
44
 
40
- export function formatStatusLine({
41
- engine,
42
- sessionState,
43
- sessionStats = null,
44
- sessionSource = "pi",
45
+ export function formatStatusLine(options) {
46
+ return formatStatusLines(options).join(" ");
47
+ }
48
+
49
+ export function formatStatusLines({
45
50
  extensionDiagnostics = [],
46
51
  lifecycleState = null,
47
- gitBranch = null,
48
52
  }) {
49
- const statsSessionId = sessionStats?.sessionId ?? sessionState?.sessionId ?? "unknown";
50
- const tokens = sessionStats?.tokens
51
- ? `${sessionStats.tokens.input ?? 0}in/${sessionStats.tokens.output ?? 0}out`
52
- : "n/a";
53
- const parts = [
54
- `git:${gitBranch || "none"}`,
55
- `session:${statsSessionId}`,
56
- `source:${sessionSource}`,
57
- ];
58
- if (engine.sessionName) parts.push(`name:${engine.sessionName}`);
59
- const remoteMemories = engine.remoteMemorySources ?? [];
60
- if (remoteMemories.length > 0) parts.push(`remote-memory:${remoteMemories.map((source) => source.name).join(",")}`);
61
- parts.push(
62
- `model:${engine.modelId}`,
63
- `provider:${engine.provider}`,
64
- `thinking:${engine.thinkingLevel ?? "unknown"}`,
65
- `tokens:${tokens}`,
66
- `ext:${formatExtensionDiagnosticSummary(extensionDiagnostics, lifecycleState)}`,
67
- );
68
- return parts.join(" ");
53
+ const diagnosticSummary = formatExtensionDiagnosticSummary(extensionDiagnostics, lifecycleState);
54
+ return shouldShowDiagnostics(diagnosticSummary) ? [`Extensions: ${diagnosticSummary}`] : [];
55
+ }
56
+
57
+ export function formatProviderQuotaLines(providerQuota, { width = 20 } = {}) {
58
+ const windows = providerQuota?.limits?.flatMap((limit) => limit.windows ?? []) ?? [];
59
+ return windows.slice(0, 2).map((window) => formatProviderQuotaLine(window, { width }));
60
+ }
61
+
62
+ export function formatProviderQuotaLine(window, { width = 20 } = {}) {
63
+ const label = window.label === "weekly" ? "Weekly limit:" : `${window.label} limit:`;
64
+ const left = formatPercent(window.remainingPercent);
65
+ return `${label.padEnd(28)} ${formatQuotaBar(window.remainingPercent, width)} ${left}% left (${formatQuotaReset(window.resetsAt)})`;
66
+ }
67
+
68
+ export function formatQuotaBar(percent, width = 20) {
69
+ const value = Math.max(0, Math.min(100, Number(percent) || 0));
70
+ const filled = Math.round((value / 100) * width);
71
+ return `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`;
72
+ }
73
+
74
+ export function formatQuotaReset(resetsAt) {
75
+ if (!resetsAt) return "reset unknown";
76
+ const date = new Date(resetsAt);
77
+ if (Number.isNaN(date.getTime())) return "reset unknown";
78
+ return `resets ${formatResetDate(date)}`;
79
+ }
80
+
81
+ function formatResetDate(date) {
82
+ const hours = String(date.getHours()).padStart(2, "0");
83
+ const minutes = String(date.getMinutes()).padStart(2, "0");
84
+ const day = date.getDate();
85
+ const month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getMonth()];
86
+ return `${hours}:${minutes} on ${day} ${month}`;
69
87
  }
70
88
 
71
89
  export function formatStatusBarLine({
@@ -154,6 +172,10 @@ export function formatCompactTokenCount(tokens) {
154
172
  return `${formatOneDecimal(value / 1000000)}M`;
155
173
  }
156
174
 
175
+ function shouldShowDiagnostics(summary) {
176
+ return summary !== "ok" && summary.split(",").some((part) => !part.endsWith("info"));
177
+ }
178
+
157
179
  export function formatExtensionDiagnosticSummary(extensionDiagnostics = [], lifecycleState = null) {
158
180
  const diagnostics = [...extensionDiagnostics, ...(lifecycleState?.diagnostics ?? [])];
159
181
  if (diagnostics.length === 0) return "ok";
@@ -194,3 +216,7 @@ function formatOneDecimal(value) {
194
216
  const rounded = Math.round(value * 10) / 10;
195
217
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
196
218
  }
219
+
220
+ function formatPercent(value) {
221
+ return Math.round(Math.max(0, Math.min(100, Number(value) || 0)));
222
+ }
@@ -1,8 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  const HISTORY_VERSION = 1;
5
5
  const MAX_HISTORY_ITEMS = 100;
6
+ const LOCK_WAIT_MS = 2000;
7
+ const LOCK_STALE_MS = 5000;
6
8
 
7
9
  export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } = {}) {
8
10
  return {
@@ -20,8 +22,10 @@ export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } =
20
22
  save(items) {
21
23
  if (!path) return;
22
24
  const normalized = normalizeItems(items, maxItems);
23
- mkdirSync(dirname(path), { recursive: true });
24
- writeFileSync(path, `${JSON.stringify({ version: HISTORY_VERSION, items: normalized }, null, 2)}\n`, "utf8");
25
+ withHistoryLock(path, () => {
26
+ const merged = mergeItems(normalized, this.load(), maxItems);
27
+ writeHistoryFile(path, merged);
28
+ });
25
29
  },
26
30
  };
27
31
  }
@@ -33,3 +37,61 @@ function normalizeItems(items, maxItems) {
33
37
  .filter(Boolean)
34
38
  .slice(0, maxItems);
35
39
  }
40
+
41
+ function mergeItems(primaryItems, existingItems, maxItems) {
42
+ const merged = [];
43
+ const seen = new Set();
44
+ for (const item of [...primaryItems, ...existingItems]) {
45
+ if (seen.has(item)) continue;
46
+ seen.add(item);
47
+ merged.push(item);
48
+ if (merged.length >= maxItems) break;
49
+ }
50
+ return merged;
51
+ }
52
+
53
+ function writeHistoryFile(path, items) {
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
56
+ const payload = `${JSON.stringify({ version: HISTORY_VERSION, items }, null, 2)}\n`;
57
+ writeFileSync(tmpPath, payload, "utf8");
58
+ renameSync(tmpPath, path);
59
+ }
60
+
61
+ function withHistoryLock(path, fn) {
62
+ mkdirSync(dirname(path), { recursive: true });
63
+ const lockPath = `${path}.lock`;
64
+ const fd = acquireLock(lockPath);
65
+ try {
66
+ return fn();
67
+ } finally {
68
+ closeSync(fd);
69
+ try { unlinkSync(lockPath); } catch {}
70
+ }
71
+ }
72
+
73
+ function acquireLock(lockPath) {
74
+ const start = Date.now();
75
+ while (true) {
76
+ try {
77
+ const fd = openSync(lockPath, "wx");
78
+ writeFileSync(fd, `${process.pid}\n`, "utf8");
79
+ return fd;
80
+ } catch (err) {
81
+ if (err?.code !== "EEXIST") throw err;
82
+ removeStaleLock(lockPath);
83
+ if (Date.now() - start >= LOCK_WAIT_MS) throw err;
84
+ sleepSync(25);
85
+ }
86
+ }
87
+ }
88
+
89
+ function removeStaleLock(lockPath) {
90
+ try {
91
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) unlinkSync(lockPath);
92
+ } catch {}
93
+ }
94
+
95
+ function sleepSync(ms) {
96
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
97
+ }
@@ -14,12 +14,13 @@ export async function runSingleShotPrompt({
14
14
  refreshStatusBar,
15
15
  modeState = null,
16
16
  }) {
17
- const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
18
- ui.writeln(turnInput.displayMessage);
19
- ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
20
- if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
21
- refreshStatusBar.startWorking?.();
17
+ memoryStore.beginTurn();
22
18
  try {
19
+ const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
20
+ ui.writeln(turnInput.displayMessage);
21
+ ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
22
+ if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
23
+ refreshStatusBar.startWorking?.();
23
24
  const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
24
25
  renderPendingAssistantRecallPreview({ runner, ui });
25
26
  await handleTurnLifecycleAction(result?.lifecycleAction, { runner, ui });
@@ -127,8 +128,9 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
127
128
  }
128
129
 
129
130
  async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
130
- const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
131
+ memoryStore.beginTurn();
131
132
  try {
133
+ const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
132
134
  ui.writeln(turnInput.displayMessage);
133
135
  ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
134
136
  if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
@@ -1,7 +1,6 @@
1
1
  import { basename, join, resolve } from "node:path";
2
2
  import { existsSync, mkdirSync } from "node:fs";
3
3
  import { createUI } from "../ui.mjs";
4
- import { createPermissionController, MODE } from "../permissions.mjs";
5
4
  import { loadKeybindings } from "../input/keybindings.mjs";
6
5
  import { createInputHistoryStore } from "../input/history-store.mjs";
7
6
  import { createModeState } from "../input/mode-state.mjs";
@@ -12,21 +11,16 @@ import { createMarchAuthStorage } from "../../auth/storage.mjs";
12
11
  import { createRuntimeRunner } from "./create-runtime-runner.mjs";
13
12
  import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
14
13
  import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
15
- import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
16
14
  import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
17
15
  import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
18
16
  import { loadOrCreateProjectId, resumeStartupSession } from "./startup-session.mjs";
19
- import { initializeMcp } from "../../mcp/index.mjs";
20
- import { createWebToolsFromConfig } from "../../web/tools.mjs";
21
- import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
22
17
  import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
23
18
  import { defaultProfilePaths, ensureProfileFiles } from "../../context/profiles.mjs";
24
- import { createDesktopTurnNotifier } from "../../notification/desktop-notifier.mjs";
25
19
  import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
26
20
  import { resolveMemoryRoot } from "../../memory/root.mjs";
27
21
  import { ensureBrowserDaemon } from "../../browser/client/lifecycle.mjs";
28
22
 
29
- export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot, useRuntimeProcess } = {}) {
23
+ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot } = {}) {
30
24
  if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
31
25
  await ensureBrowserDaemon({ stateRoot }).catch(() => {});
32
26
 
@@ -67,23 +61,10 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
67
61
  ensureProfileFiles(profilePaths);
68
62
  const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
69
63
  const remoteMemorySources = normalizeRemoteMemorySources(config);
70
- const memoryTools = createMarkdownMemoryTools(memoryStore, { remoteSources: remoteMemorySources });
71
64
  const currentProject = basename(cwd);
72
65
  const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
73
66
 
74
- const mcpInit = useRuntimeProcess
75
- ? { clientManager: null, mcpTools: [], mcpInjections: [], errors: [] }
76
- : await initializeMcp({ projectDir: cwd });
77
- for (const { server, error } of mcpInit.errors) {
78
- if (!args.json) process.stderr.write(`[mcp] ${server}: ${error}\n`);
79
- }
80
-
81
- const webTools = createWebToolsFromConfig(config);
82
- const turnNotifier = createDesktopTurnNotifier({ enabled: Boolean(config.notifications?.turnEnd), config: config.notifications });
83
- const permissionMode = args.permissionMode ?? MODE.BYPASS;
84
- const permissionController = createPermissionController({ mode: permissionMode });
85
- const usePiSessions = true;
86
- const usePiRuntimeHost = true;
67
+ const permissionMode = args.permissionMode;
87
68
  const sessionSource = "pi";
88
69
  const sessionsRoot = join(projectMarchDir, "sessions");
89
70
  const sessionState = {
@@ -92,10 +73,6 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
92
73
  };
93
74
  sessionState.sessionDir = join(sessionsRoot, sessionState.sessionId);
94
75
  const contextDumpRoot = resolve(projectMarchDir, "context-dumps", sessionState.sessionId);
95
- const modelContextDumper = createModelContextDumper({
96
- enabled: args.dumpContext,
97
- rootDir: contextDumpRoot,
98
- });
99
76
 
100
77
  const ui = createUI({
101
78
  json: args.json,
@@ -132,10 +109,9 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
132
109
  let runner;
133
110
  try {
134
111
  runner = await createRuntimeRunner({
135
- useRuntimeProcess, runnerOptions, ui, memoryStore, memoryTools, shellRuntime,
136
- mcpTools: mcpInit.mcpTools, mcpInjections: mcpInit.mcpInjections, mcpClientManager: mcpInit.clientManager, webTools,
137
- usePiSessions, usePiRuntimeHost, authStorage: authConfig.authStorage,
138
- permissionController, modelContextDumper, turnNotifier, logger,
112
+ runnerOptions,
113
+ ui,
114
+ shellRuntime,
139
115
  refreshStatusBar: (...args) => refreshStatusBar?.(...args),
140
116
  });
141
117
  } catch (err) {
@@ -1,61 +1,19 @@
1
- import { createRunner } from "../../agent/runner.mjs";
2
1
  import { createRunnerProcessClient } from "../../agent/runtime/runner-process-client.mjs";
3
- import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
4
2
 
5
3
  export async function createRuntimeRunner({
6
- useRuntimeProcess = false,
7
4
  runnerOptions,
8
5
  ui,
9
- memoryStore,
10
- memoryTools,
11
6
  shellRuntime,
12
- mcpTools,
13
- mcpInjections,
14
- mcpClientManager,
15
- webTools,
16
- usePiSessions,
17
- usePiRuntimeHost,
18
- authStorage,
19
- permissionController,
20
- modelContextDumper,
21
- turnNotifier,
22
- logger,
23
7
  refreshStatusBar,
24
8
  } = {}) {
25
9
  const onModelPayload = ({ estimatedTokens }) => {
26
10
  refreshStatusBar?.({ contextTokens: estimatedTokens });
27
11
  };
12
+ const onLspStatusChange = () => {
13
+ refreshStatusBar?.();
14
+ };
28
15
 
29
- const runner = useRuntimeProcess
30
- ? (await createRunnerProcessClient({ runnerOptions, ui, onModelPayload })).runner
31
- : await createRunner({
32
- ...runnerOptions,
33
- ui,
34
- memoryStore,
35
- memoryTools,
36
- shellRuntime,
37
- mcpTools,
38
- mcpInjections,
39
- mcpClientManager,
40
- webTools,
41
- sessionManager: resolvePiSessionManager({
42
- cwd: runnerOptions.cwd,
43
- projectMarchDir: runnerOptions.projectMarchDir,
44
- enabled: usePiSessions,
45
- }),
46
- useRuntimeHost: usePiRuntimeHost,
47
- syncPiSidecar: usePiSessions || usePiRuntimeHost,
48
- authStorage,
49
- maxTurns: runnerOptions.config?.maxTurns ?? undefined,
50
- trimBatch: runnerOptions.config?.trimBatch ?? undefined,
51
- hostedTools: runnerOptions.config?.hostedTools,
52
- permissionController,
53
- modelContextDumper,
54
- turnNotifier,
55
- logger,
56
- onModelPayload,
57
- });
58
-
16
+ const { runner } = await createRunnerProcessClient({ runnerOptions, ui, onModelPayload, onLspStatusChange });
59
17
  runner.shellRuntime ??= shellRuntime;
60
18
  return runner;
61
19
  }