march-cli 0.1.38 → 0.1.40
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/runner.mjs +8 -8
- package/src/agent/runtime/runner-process-factory.mjs +1 -1
- package/src/agent/turn/turn-runner.mjs +4 -4
- package/src/cli/args.mjs +3 -0
- package/src/cli/commands/mode-command.mjs +1 -0
- package/src/cli/commands/registry/slash-command-registry.mjs +2 -3
- package/src/cli/input/keybindings.mjs +2 -0
- package/src/cli/repl-commands.mjs +1 -1
- package/src/cli/repl-loop.mjs +1 -1
- package/src/cli/session/pi-session-switch-command.mjs +11 -11
- package/src/cli/session/session-list-command.mjs +1 -1
- package/src/cli/session/session-source-command.mjs +0 -76
- package/src/cli/startup/app-runtime.mjs +52 -22
- package/src/cli/tui/output/timeline-block-restore.mjs +45 -0
- package/src/cli/tui/output-buffer.mjs +5 -0
- package/src/cli/tui/tui-input-controller.mjs +16 -0
- package/src/cli/ui.mjs +6 -2
- package/src/cli/workspace/command.mjs +11 -37
- package/src/cli/workspace/output-router.mjs +62 -36
- package/src/cli/workspace/project-runtime.mjs +2 -0
- package/src/cli/workspace/runtime-session-state.mjs +9 -0
- package/src/cli/workspace/tui-timeline-projection.mjs +179 -0
- package/src/cli/workspace/tui-timeline.mjs +247 -0
- package/src/extensions/lifecycle-adapter.mjs +2 -2
- package/src/main.mjs +7 -1
- package/src/session/sidecar-sync.mjs +3 -17
- package/src/session/sidecar.mjs +40 -41
- package/src/session/state/march-session-state.mjs +165 -0
- package/src/session/state/march-session-sync.mjs +20 -0
- package/src/session/state/march-session-ui-state.mjs +89 -0
- package/src/workspace/session-index.mjs +27 -0
- package/src/workspace/supervisor.mjs +19 -13
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +0 -10
- package/src/cli/session/session-switch-command.mjs +0 -1
- package/src/session/persist.mjs +0 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createTuiTimelineProjection } from "./tui-timeline-projection.mjs";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MAX_TIMELINE_EVENTS = 4000;
|
|
4
|
+
export const DEFAULT_TIMELINE_PERSIST_DEBOUNCE_MS = 250;
|
|
5
|
+
|
|
6
|
+
export function createTuiTimelineRegistry({
|
|
7
|
+
maxEventsPerTimeline = DEFAULT_MAX_TIMELINE_EVENTS,
|
|
8
|
+
persistDebounceMs = DEFAULT_TIMELINE_PERSIST_DEBOUNCE_MS,
|
|
9
|
+
onPersistTimeline = null,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const timelines = new Map();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
ensure(key, { events = null } = {}) {
|
|
15
|
+
let timeline = timelines.get(key);
|
|
16
|
+
if (!timeline) {
|
|
17
|
+
timeline = createTuiTimelineInstance({
|
|
18
|
+
key,
|
|
19
|
+
maxEvents: maxEventsPerTimeline,
|
|
20
|
+
persistDebounceMs,
|
|
21
|
+
onPersist: onPersistTimeline,
|
|
22
|
+
});
|
|
23
|
+
timelines.set(key, timeline);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(events)) timeline.hydrateIfEmpty(events);
|
|
26
|
+
return timeline;
|
|
27
|
+
},
|
|
28
|
+
get(key) {
|
|
29
|
+
return timelines.get(key) ?? null;
|
|
30
|
+
},
|
|
31
|
+
has(key) {
|
|
32
|
+
return timelines.has(key);
|
|
33
|
+
},
|
|
34
|
+
clear(key, options) {
|
|
35
|
+
const timeline = this.ensure(key);
|
|
36
|
+
timeline.clear(options);
|
|
37
|
+
return timeline;
|
|
38
|
+
},
|
|
39
|
+
flush(key, reason = "manual") {
|
|
40
|
+
return this.get(key)?.flushPersist(reason) ?? false;
|
|
41
|
+
},
|
|
42
|
+
flushAll(reason = "manual") {
|
|
43
|
+
let flushed = 0;
|
|
44
|
+
for (const timeline of timelines.values()) {
|
|
45
|
+
if (timeline.flushPersist(reason)) flushed += 1;
|
|
46
|
+
}
|
|
47
|
+
return flushed;
|
|
48
|
+
},
|
|
49
|
+
replaceEvents(key, events) {
|
|
50
|
+
const timeline = this.ensure(key);
|
|
51
|
+
timeline.replaceEvents(events);
|
|
52
|
+
return timeline;
|
|
53
|
+
},
|
|
54
|
+
getEvents(key) {
|
|
55
|
+
return this.get(key)?.getEvents() ?? [];
|
|
56
|
+
},
|
|
57
|
+
getBlocks(key) {
|
|
58
|
+
return this.get(key)?.getBlocks() ?? [];
|
|
59
|
+
},
|
|
60
|
+
getEventCount(key) {
|
|
61
|
+
return this.get(key)?.getEventCount() ?? 0;
|
|
62
|
+
},
|
|
63
|
+
getMetadata(key) {
|
|
64
|
+
return this.get(key)?.getMetadata() ?? null;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createTuiTimelineInstance({
|
|
70
|
+
key,
|
|
71
|
+
maxEvents = DEFAULT_MAX_TIMELINE_EVENTS,
|
|
72
|
+
persistDebounceMs = DEFAULT_TIMELINE_PERSIST_DEBOUNCE_MS,
|
|
73
|
+
onPersist = null,
|
|
74
|
+
} = {}) {
|
|
75
|
+
let events = [];
|
|
76
|
+
const projection = createTuiTimelineProjection();
|
|
77
|
+
let hydrated = false;
|
|
78
|
+
let dirty = false;
|
|
79
|
+
let lastAccessedAt = Date.now();
|
|
80
|
+
let lastUpdatedAt = null;
|
|
81
|
+
let lastPersistedAt = null;
|
|
82
|
+
let estimatedBytes = 0;
|
|
83
|
+
let persistTimer = null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
key,
|
|
87
|
+
apply(method, args, { at = Date.now(), persist = true } = {}) {
|
|
88
|
+
touch();
|
|
89
|
+
const event = { method, args, at };
|
|
90
|
+
events.push(event);
|
|
91
|
+
projection.apply(event);
|
|
92
|
+
trimToBudget();
|
|
93
|
+
dirty = true;
|
|
94
|
+
lastUpdatedAt = at;
|
|
95
|
+
if (persist) schedulePersist("debounce");
|
|
96
|
+
return event;
|
|
97
|
+
},
|
|
98
|
+
hydrateIfEmpty(nextEvents) {
|
|
99
|
+
touch();
|
|
100
|
+
if (events.length > 0) return false;
|
|
101
|
+
replaceEvents(nextEvents, { markHydrated: true });
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
replaceEvents(nextEvents) {
|
|
105
|
+
touch();
|
|
106
|
+
replaceEvents(nextEvents, { markHydrated: false });
|
|
107
|
+
},
|
|
108
|
+
clear({ flush = true } = {}) {
|
|
109
|
+
touch();
|
|
110
|
+
events = [];
|
|
111
|
+
projection.clear();
|
|
112
|
+
dirty = true;
|
|
113
|
+
lastUpdatedAt = Date.now();
|
|
114
|
+
estimatedBytes = 0;
|
|
115
|
+
if (flush) this.flushPersist("clear");
|
|
116
|
+
else schedulePersist("debounce");
|
|
117
|
+
},
|
|
118
|
+
replayTo(ui) {
|
|
119
|
+
touch();
|
|
120
|
+
if (typeof ui.restoreTimelineBlocks === "function") {
|
|
121
|
+
const blocks = projection.getBlocks();
|
|
122
|
+
ui.restoreTimelineBlocks(blocks);
|
|
123
|
+
return blocks.length;
|
|
124
|
+
}
|
|
125
|
+
for (const event of events) applyRenderEvent(ui, event);
|
|
126
|
+
return events.length;
|
|
127
|
+
},
|
|
128
|
+
flushPersist(reason = "manual") {
|
|
129
|
+
clearPersistTimer();
|
|
130
|
+
if (!dirty || typeof onPersist !== "function") return false;
|
|
131
|
+
onPersist({ key, events: this.getEvents(), reason, timeline: this.getMetadata() });
|
|
132
|
+
dirty = false;
|
|
133
|
+
lastPersistedAt = Date.now();
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
getEvents() {
|
|
137
|
+
touch();
|
|
138
|
+
return cloneEvents(events);
|
|
139
|
+
},
|
|
140
|
+
getBlocks() {
|
|
141
|
+
touch();
|
|
142
|
+
return projection.getBlocks();
|
|
143
|
+
},
|
|
144
|
+
getEventCount() {
|
|
145
|
+
return events.length;
|
|
146
|
+
},
|
|
147
|
+
markPersisted() {
|
|
148
|
+
clearPersistTimer();
|
|
149
|
+
dirty = false;
|
|
150
|
+
lastPersistedAt = Date.now();
|
|
151
|
+
},
|
|
152
|
+
getMetadata() {
|
|
153
|
+
return buildMetadata();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function schedulePersist(reason) {
|
|
158
|
+
if (typeof onPersist !== "function") return;
|
|
159
|
+
clearPersistTimer();
|
|
160
|
+
persistTimer = setTimeout(() => {
|
|
161
|
+
persistTimer = null;
|
|
162
|
+
if (!dirty) return;
|
|
163
|
+
onPersist({ key, events: cloneEvents(events), reason, timeline: buildMetadata() });
|
|
164
|
+
dirty = false;
|
|
165
|
+
lastPersistedAt = Date.now();
|
|
166
|
+
}, Math.max(0, persistDebounceMs));
|
|
167
|
+
persistTimer.unref?.();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function clearPersistTimer() {
|
|
171
|
+
if (!persistTimer) return;
|
|
172
|
+
clearTimeout(persistTimer);
|
|
173
|
+
persistTimer = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function replaceEvents(nextEvents, { markHydrated }) {
|
|
177
|
+
clearPersistTimer();
|
|
178
|
+
events = normalizeTimelineEvents(nextEvents);
|
|
179
|
+
trimToBudget();
|
|
180
|
+
rebuildProjection();
|
|
181
|
+
hydrated = markHydrated;
|
|
182
|
+
dirty = false;
|
|
183
|
+
lastUpdatedAt = events.at(-1)?.at ?? null;
|
|
184
|
+
lastPersistedAt = null;
|
|
185
|
+
updateEstimatedBytes();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function trimToBudget() {
|
|
189
|
+
if (events.length > maxEvents) {
|
|
190
|
+
events.splice(0, events.length - maxEvents);
|
|
191
|
+
rebuildProjection();
|
|
192
|
+
}
|
|
193
|
+
updateEstimatedBytes();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function rebuildProjection() {
|
|
197
|
+
projection.rebuild(events);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function updateEstimatedBytes() {
|
|
201
|
+
estimatedBytes = estimateJsonBytes(events) + estimateJsonBytes(projection.getBlocks());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function touch() {
|
|
205
|
+
lastAccessedAt = Date.now();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildMetadata() {
|
|
209
|
+
return {
|
|
210
|
+
key,
|
|
211
|
+
eventCount: events.length,
|
|
212
|
+
maxEvents,
|
|
213
|
+
hydrated,
|
|
214
|
+
dirty,
|
|
215
|
+
lastAccessedAt,
|
|
216
|
+
lastUpdatedAt,
|
|
217
|
+
lastPersistedAt,
|
|
218
|
+
estimatedBytes,
|
|
219
|
+
persistScheduled: Boolean(persistTimer),
|
|
220
|
+
...projection.getMetadata(),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function normalizeTimelineEvents(events) {
|
|
226
|
+
if (!Array.isArray(events)) return [];
|
|
227
|
+
return events
|
|
228
|
+
.filter((event) => typeof event?.method === "string" && Array.isArray(event.args))
|
|
229
|
+
.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function applyRenderEvent(ui, { method, args }) {
|
|
233
|
+
const value = ui[method];
|
|
234
|
+
if (typeof value === "function") value.apply(ui, args);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cloneEvents(items) {
|
|
238
|
+
return items.map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function estimateJsonBytes(value) {
|
|
242
|
+
try {
|
|
243
|
+
return JSON.stringify(value).length;
|
|
244
|
+
} catch {
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -7,7 +7,7 @@ export const MARCH_LIFECYCLE_LAYERS = Object.freeze([
|
|
|
7
7
|
{
|
|
8
8
|
name: "march-agent-runtime",
|
|
9
9
|
owner: "March runner",
|
|
10
|
-
effects: Object.freeze(["read-session-ref", "read-
|
|
10
|
+
effects: Object.freeze(["read-session-ref", "read-session-state", "read-runtime-state"]),
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
name: "march-collaboration",
|
|
@@ -24,7 +24,7 @@ export const DEFAULT_MARCH_HOOK_POLICY = Object.freeze({
|
|
|
24
24
|
"read-agent-ref",
|
|
25
25
|
"read-workspace-ref",
|
|
26
26
|
"read-session-ref",
|
|
27
|
-
"read-
|
|
27
|
+
"read-session-state",
|
|
28
28
|
"read-diff-metadata",
|
|
29
29
|
"read-runtime-diagnostics",
|
|
30
30
|
"write-diagnostics",
|
package/src/main.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join, relative } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -13,6 +14,8 @@ import { installNetworkEnvironment } from "./network/environment.mjs";
|
|
|
13
14
|
import { runEarlyCliCommand } from "./cli/startup/early-command.mjs";
|
|
14
15
|
import { maybeRunGatewayDaemonCommand } from "./cli/startup/gateway-daemon-command.mjs";
|
|
15
16
|
|
|
17
|
+
const { version: packageVersion } = createRequire(import.meta.url)("../package.json");
|
|
18
|
+
|
|
16
19
|
export async function run(argv) {
|
|
17
20
|
const cwd = process.cwd();
|
|
18
21
|
loadDotEnv(cwd);
|
|
@@ -23,6 +26,10 @@ export async function run(argv) {
|
|
|
23
26
|
showHelp();
|
|
24
27
|
return 0;
|
|
25
28
|
}
|
|
29
|
+
if (args.version) {
|
|
30
|
+
process.stdout.write(`${packageVersion}\n`);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
26
33
|
|
|
27
34
|
const config = loadConfig(cwd);
|
|
28
35
|
const stateRoot = join(homedir(), ".march");
|
|
@@ -67,7 +74,6 @@ export async function run(argv) {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
const dumpContextPath = args.dumpContext ? relative(cwd, app.contextDumpRoot) : null;
|
|
70
|
-
if (app.startupResume.transcriptTurns?.length > 0) app.ui.restoreTranscript?.(app.startupResume.transcriptTurns);
|
|
71
77
|
for (const line of formatStartupBanner({ cwd, modelId: app.runner.engine.modelId, thinkingLevel: app.runner.engine.thinkingLevel, mode: app.modeState.get(), dumpContextPath })) app.ui.writeln(line);
|
|
72
78
|
try {
|
|
73
79
|
await runInteractiveRepl({
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { syncMarchSessionState } from "./state/march-session-sync.mjs";
|
|
2
2
|
|
|
3
|
-
export function syncPiSessionSidecar(
|
|
4
|
-
|
|
5
|
-
return null;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
return savePiSessionSidecar({
|
|
9
|
-
projectMarchDir,
|
|
10
|
-
sessionRef: sessionStats.sessionFile,
|
|
11
|
-
engine,
|
|
12
|
-
metadata: {
|
|
13
|
-
sessionId: sessionStats.sessionId,
|
|
14
|
-
sessionFile: sessionStats.sessionFile,
|
|
15
|
-
runtimeHost: Boolean(sessionStats.runtimeHost),
|
|
16
|
-
...metadata,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
3
|
+
export function syncPiSessionSidecar(options) {
|
|
4
|
+
return syncMarchSessionState(options);
|
|
19
5
|
}
|
package/src/session/sidecar.mjs
CHANGED
|
@@ -1,69 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
captureMarchSessionState,
|
|
3
|
+
getLegacyPiSidecarDir,
|
|
4
|
+
getLegacyPiSidecarPath,
|
|
5
|
+
loadMarchSessionStateForPiBackend,
|
|
6
|
+
loadLegacyPiSidecar,
|
|
7
|
+
saveMarchSessionStateValue,
|
|
8
|
+
} from "./state/march-session-state.mjs";
|
|
3
9
|
|
|
4
10
|
export const PI_SIDECAR_VERSION = 1;
|
|
5
11
|
|
|
6
12
|
export function getPiSidecarDir(projectMarchDir) {
|
|
7
|
-
return
|
|
13
|
+
return getLegacyPiSidecarDir(projectMarchDir);
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
export function getPiSidecarPath(projectMarchDir, sessionRef) {
|
|
11
|
-
return
|
|
17
|
+
return getLegacyPiSidecarPath(projectMarchDir, sessionRef);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export function captureContextSidecar(engine, metadata = {}) {
|
|
15
|
-
return {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pendingAssistantRecallHints: engine.pendingAssistantRecallHints ?? [],
|
|
26
|
-
turns: engine.turns,
|
|
27
|
-
};
|
|
21
|
+
return captureMarchSessionState(engine, {
|
|
22
|
+
sessionId: metadata.sessionId,
|
|
23
|
+
backend: {
|
|
24
|
+
type: "pi",
|
|
25
|
+
sessionId: metadata.sessionId ?? null,
|
|
26
|
+
sessionFile: metadata.sessionFile ?? null,
|
|
27
|
+
runtimeHost: Boolean(metadata.runtimeHost),
|
|
28
|
+
},
|
|
29
|
+
metadata,
|
|
30
|
+
});
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function savePiSessionSidecar({ projectMarchDir, sessionRef, engine, metadata = {} }) {
|
|
31
34
|
return savePiSessionSidecarState({
|
|
32
35
|
projectMarchDir,
|
|
33
36
|
sessionRef,
|
|
34
|
-
state: captureContextSidecar(engine, metadata),
|
|
37
|
+
state: captureContextSidecar(engine, { sessionFile: sessionRef, ...metadata }),
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export function savePiSessionSidecarState({ projectMarchDir, sessionRef, state }) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { path, state };
|
|
42
|
+
return saveMarchSessionStateValue({
|
|
43
|
+
projectMarchDir,
|
|
44
|
+
sessionId: state.sessionId ?? state.backend?.sessionId ?? state.sessionFile ?? sessionRef,
|
|
45
|
+
state: normalizeLegacyState(state, sessionRef),
|
|
46
|
+
});
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function loadPiSessionSidecar({ projectMarchDir, sessionRef }) {
|
|
48
|
-
|
|
49
|
-
if (!existsSync(path)) return null;
|
|
50
|
-
const state = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
-
if (!isValidSidecarState(state)) {
|
|
52
|
-
throw new Error("Invalid pi session sidecar");
|
|
53
|
-
}
|
|
54
|
-
return { path, state };
|
|
50
|
+
return loadMarchSessionStateForPiBackend({ projectMarchDir, sessionId: null, sessionRef }) ?? loadLegacyPiSidecar({ projectMarchDir, sessionRef });
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
function
|
|
58
|
-
|
|
53
|
+
export function loadPiSessionContextState({ projectMarchDir, sessionRef, sessionId = null }) {
|
|
54
|
+
return loadMarchSessionStateForPiBackend({ projectMarchDir, sessionId, sessionRef });
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
function
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
function normalizeLegacyState(state, sessionRef) {
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
sessionId: state.sessionId ?? state.backend?.sessionId ?? state.sessionFile ?? sessionRef,
|
|
61
|
+
backend: state.backend ?? {
|
|
62
|
+
type: "pi",
|
|
63
|
+
sessionId: state.sessionId ?? null,
|
|
64
|
+
sessionFile: state.sessionFile ?? sessionRef,
|
|
65
|
+
runtimeHost: Boolean(state.runtimeHost),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
69
68
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { loadPiSessionTranscriptTurns } from "../transcript.mjs";
|
|
4
|
+
|
|
5
|
+
export const MARCH_SESSION_STATE_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export function getMarchSessionStateRoot(projectMarchDir) {
|
|
8
|
+
return join(projectMarchDir, "sessions");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getMarchSessionStateDir(projectMarchDir, sessionId) {
|
|
12
|
+
return join(getMarchSessionStateRoot(projectMarchDir), normalizeSessionId(sessionId));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getMarchSessionStatePath(projectMarchDir, sessionId) {
|
|
16
|
+
return join(getMarchSessionStateDir(projectMarchDir, sessionId), "state.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function captureMarchSessionState(engine, { sessionId, backend = null, metadata = {} } = {}) {
|
|
20
|
+
return {
|
|
21
|
+
version: MARCH_SESSION_STATE_VERSION,
|
|
22
|
+
savedAt: new Date().toISOString(),
|
|
23
|
+
sessionId: sessionId ?? metadata.sessionId ?? backend?.sessionId ?? null,
|
|
24
|
+
backend,
|
|
25
|
+
...metadata,
|
|
26
|
+
cwd: engine.cwd,
|
|
27
|
+
modelId: engine.modelId,
|
|
28
|
+
provider: engine.provider,
|
|
29
|
+
sessionName: engine.sessionName ?? "",
|
|
30
|
+
thinkingLevel: engine.thinkingLevel,
|
|
31
|
+
namespace: engine.namespace,
|
|
32
|
+
pendingAssistantRecallHints: engine.pendingAssistantRecallHints ?? [],
|
|
33
|
+
turns: engine.turns,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function saveMarchSessionState({ projectMarchDir, sessionId, engine, backend = null, metadata = {} }) {
|
|
38
|
+
return saveMarchSessionStateValue({
|
|
39
|
+
projectMarchDir,
|
|
40
|
+
sessionId,
|
|
41
|
+
state: captureMarchSessionState(engine, { sessionId, backend, metadata }),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function saveMarchSessionStateValue({ projectMarchDir, sessionId, state }) {
|
|
46
|
+
if (!sessionId) throw new Error("March session id is required");
|
|
47
|
+
const existing = loadMarchSessionState({ projectMarchDir, sessionId })?.state ?? null;
|
|
48
|
+
const nextState = normalizeMarchSessionStateForSave({ ...existing, ...state });
|
|
49
|
+
validateMarchSessionState(nextState);
|
|
50
|
+
const dir = getMarchSessionStateDir(projectMarchDir, sessionId);
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
52
|
+
const path = getMarchSessionStatePath(projectMarchDir, sessionId);
|
|
53
|
+
writeFileSync(path, JSON.stringify({ ...nextState, sessionId: nextState.sessionId ?? sessionId }, null, 2), "utf8");
|
|
54
|
+
return { path, state: { ...nextState, sessionId: nextState.sessionId ?? sessionId } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadMarchSessionState({ projectMarchDir, sessionId }) {
|
|
58
|
+
const path = getMarchSessionStatePath(projectMarchDir, sessionId);
|
|
59
|
+
if (!existsSync(path)) return null;
|
|
60
|
+
const state = normalizeMarchSessionStateForSave(JSON.parse(readFileSync(path, "utf8")));
|
|
61
|
+
if (!isValidMarchSessionState(state)) throw new Error("Invalid March session state");
|
|
62
|
+
return { path, state };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function loadMarchSessionContextState({ projectMarchDir, sessionId, backendSessionFile = null }) {
|
|
66
|
+
const stored = loadMarchSessionState({ projectMarchDir, sessionId });
|
|
67
|
+
if (!stored) return null;
|
|
68
|
+
const sessionFile = backendSessionFile ?? stored.state.backend?.sessionFile ?? stored.state.sessionFile ?? null;
|
|
69
|
+
return { ...stored, state: withBackendTranscriptTurns(stored.state, sessionFile) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function listMarchSessionStates({ projectMarchDir }) {
|
|
73
|
+
const root = getMarchSessionStateRoot(projectMarchDir);
|
|
74
|
+
if (!existsSync(root)) return [];
|
|
75
|
+
return readdirSync(root, { withFileTypes: true })
|
|
76
|
+
.filter((entry) => entry.isDirectory())
|
|
77
|
+
.map((entry) => {
|
|
78
|
+
try {
|
|
79
|
+
return loadMarchSessionState({ projectMarchDir, sessionId: entry.name });
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function loadMarchSessionStateForPiBackend({ projectMarchDir, sessionId, sessionRef }) {
|
|
88
|
+
const marchState = sessionId ? loadMarchSessionContextState({ projectMarchDir, sessionId, backendSessionFile: sessionRef }) : null;
|
|
89
|
+
if (marchState) return marchState;
|
|
90
|
+
const matchingState = findMarchSessionStateByPiBackend({ projectMarchDir, sessionRef });
|
|
91
|
+
if (matchingState) return { ...matchingState, state: withBackendTranscriptTurns(matchingState.state, sessionRef) };
|
|
92
|
+
return loadLegacyPiSidecarContextState({ projectMarchDir, sessionRef });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findMarchSessionStateByPiBackend({ projectMarchDir, sessionRef }) {
|
|
96
|
+
const normalizedRef = normalizeSessionRef(sessionRef);
|
|
97
|
+
return listMarchSessionStates({ projectMarchDir }).find(({ state }) => {
|
|
98
|
+
const sessionFile = state.backend?.sessionFile ?? state.sessionFile ?? null;
|
|
99
|
+
return sessionFile && normalizeSessionRef(sessionFile) === normalizedRef;
|
|
100
|
+
}) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getLegacyPiSidecarDir(projectMarchDir) {
|
|
104
|
+
return join(projectMarchDir, "pi-sidecars");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getLegacyPiSidecarPath(projectMarchDir, sessionRef) {
|
|
108
|
+
return join(getLegacyPiSidecarDir(projectMarchDir), `${normalizeSessionRef(sessionRef)}.json`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadLegacyPiSidecar({ projectMarchDir, sessionRef }) {
|
|
112
|
+
const path = getLegacyPiSidecarPath(projectMarchDir, sessionRef);
|
|
113
|
+
if (!existsSync(path)) return null;
|
|
114
|
+
const state = normalizeMarchSessionStateForSave(JSON.parse(readFileSync(path, "utf8")));
|
|
115
|
+
if (!isValidMarchSessionState(state)) throw new Error("Invalid March session state");
|
|
116
|
+
return { path, state };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function loadLegacyPiSidecarContextState({ projectMarchDir, sessionRef }) {
|
|
120
|
+
const legacy = loadLegacyPiSidecar({ projectMarchDir, sessionRef });
|
|
121
|
+
if (!legacy) return null;
|
|
122
|
+
return { ...legacy, state: withBackendTranscriptTurns(legacy.state, sessionRef) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function normalizeSessionId(sessionId) {
|
|
126
|
+
const value = String(sessionId ?? "").trim();
|
|
127
|
+
if (!value || value === "." || value === "..") throw new Error("Invalid March session id");
|
|
128
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateMarchSessionState(state) {
|
|
132
|
+
if (!isValidMarchSessionState(state)) throw new Error("Invalid March session state");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isValidMarchSessionState(state) {
|
|
136
|
+
return state?.version === MARCH_SESSION_STATE_VERSION
|
|
137
|
+
&& Boolean(state.cwd)
|
|
138
|
+
&& Array.isArray(state.turns);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeMarchSessionStateForSave(state) {
|
|
142
|
+
const { renderTimeline: _renderTimeline, renderTimelineUpdatedAt: _renderTimelineUpdatedAt, ...coreState } = state ?? {};
|
|
143
|
+
return coreState;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeSessionRef(sessionRef) {
|
|
147
|
+
const ref = basename(String(sessionRef).trim()).replace(/\.jsonl$/i, "");
|
|
148
|
+
if (!ref || ref === "." || ref === "..") throw new Error("Invalid pi session reference");
|
|
149
|
+
return ref.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function withBackendTranscriptTurns(state, sessionFile) {
|
|
153
|
+
if (!sessionFile) return { ...state };
|
|
154
|
+
let transcriptTurns = [];
|
|
155
|
+
try {
|
|
156
|
+
transcriptTurns = loadPiSessionTranscriptTurns(sessionFile);
|
|
157
|
+
} catch {
|
|
158
|
+
return { ...state };
|
|
159
|
+
}
|
|
160
|
+
if (transcriptTurns.length <= (state.turns?.length ?? 0)) return { ...state };
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
turns: transcriptTurns,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { saveMarchSessionState } from "./march-session-state.mjs";
|
|
2
|
+
|
|
3
|
+
export function syncMarchSessionState({ enabled = false, projectMarchDir, engine, sessionStats, metadata = {} }) {
|
|
4
|
+
if (!enabled || !projectMarchDir || !sessionStats?.persisted || !sessionStats.sessionId) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return saveMarchSessionState({
|
|
9
|
+
projectMarchDir,
|
|
10
|
+
sessionId: sessionStats.sessionId,
|
|
11
|
+
engine,
|
|
12
|
+
backend: {
|
|
13
|
+
type: "pi",
|
|
14
|
+
sessionId: sessionStats.sessionId,
|
|
15
|
+
sessionFile: sessionStats.sessionFile ?? null,
|
|
16
|
+
runtimeHost: Boolean(sessionStats.runtimeHost),
|
|
17
|
+
},
|
|
18
|
+
metadata,
|
|
19
|
+
});
|
|
20
|
+
}
|