march-cli 0.1.39 → 0.1.41

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.
@@ -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
+ }
@@ -38,6 +38,26 @@ export function upsertProviderProfile({ path = globalConfigJsonPath(), id, type,
38
38
  return config;
39
39
  }
40
40
 
41
+ export function removeProviderProfile({ path = globalConfigJsonPath(), id }) {
42
+ const config = readConfigJson(path);
43
+ const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
44
+ ? config.providers
45
+ : {};
46
+ const hadProviderProfile = Object.prototype.hasOwnProperty.call(providers, id);
47
+ const wasSelectedProvider = config.provider === id;
48
+ if (hadProviderProfile) delete providers[id];
49
+ if (Object.keys(providers).length) config.providers = providers;
50
+ else delete config.providers;
51
+ if (wasSelectedProvider) {
52
+ delete config.provider;
53
+ delete config.model;
54
+ delete config.serviceTier;
55
+ }
56
+ if (!hadProviderProfile && !wasSelectedProvider) return false;
57
+ writeConfigJson(path, config);
58
+ return true;
59
+ }
60
+
41
61
  export function upsertSharedProviderProfile({ path = globalConfigJsonPath(), id, provider }) {
42
62
  const config = readConfigJson(path);
43
63
  const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { runProviderConfigCommand } from "./config-command.mjs";
3
3
  import { runProviderShareCommand } from "./share-command.mjs";
4
4
  import { runProviderAcceptCommand } from "./accept-command.mjs";
5
+ import { runProviderRemoveCommand } from "./remove-command.mjs";
5
6
 
6
7
  export async function runProviderCommand(args, { homeDir = homedir(), stderr = process.stderr } = {}) {
7
8
  if (args.providerConfig) return await runProviderConfigCommand({ homeDir });
@@ -16,6 +17,9 @@ export async function runProviderCommand(args, { homeDir = homedir(), stderr = p
16
17
  if (args.command.args[0] === "accept") {
17
18
  return await runProviderAcceptCommand({ homeDir, token: args.command.args[1] });
18
19
  }
19
- stderr.write("Usage: march provider --config | march provider share [id] | march provider accept <token>\n");
20
+ if (args.command.args[0] === "remove" || args.command.args[0] === "uninstall") {
21
+ return await runProviderRemoveCommand({ homeDir, providerId: args.command.args[1] });
22
+ }
23
+ stderr.write("Usage: march provider --config | march provider share [id] | march provider accept <token> | march provider remove\n");
20
24
  return 1;
21
25
  }
@@ -0,0 +1,129 @@
1
+ import { createInterface } from "node:readline";
2
+ import { AuthStorage } from "@earendil-works/pi-coding-agent";
3
+ import { globalConfigJsonPath, readConfigJson, removeProviderProfile } from "../config/config-json.mjs";
4
+ import { getMarchAuthPath } from "../auth/storage.mjs";
5
+ import { selectWithKeyboard } from "../cli/input/select-with-keyboard.mjs";
6
+ import { getProviderLabel } from "./presets.mjs";
7
+
8
+ export async function runProviderRemoveCommand({
9
+ homeDir,
10
+ providerId,
11
+ input = process.stdin,
12
+ output = process.stdout,
13
+ select = selectWithKeyboard,
14
+ confirm = confirmRemoval,
15
+ authStorage = AuthStorage.create(getMarchAuthPath(homeDir)),
16
+ } = {}) {
17
+ const removableProviders = listRemovableProviders({ homeDir, authStorage });
18
+ const selectedProviderId = providerId ?? await selectProviderToRemove({ removableProviders, input, output, select });
19
+ if (!selectedProviderId) {
20
+ if (!removableProviders.length) {
21
+ output.write("No configured providers to remove.\nRun `march provider --config` to add one.\n");
22
+ } else {
23
+ output.write("Provider removal cancelled.\n");
24
+ }
25
+ return 1;
26
+ }
27
+
28
+ const provider = removableProviders.find((item) => item.id === selectedProviderId) ?? {
29
+ id: selectedProviderId,
30
+ label: getProviderLabel(selectedProviderId),
31
+ sources: [],
32
+ };
33
+ const confirmed = await confirm({ input, output, provider });
34
+ if (!confirmed) {
35
+ output.write("Provider removal cancelled.\n");
36
+ return 1;
37
+ }
38
+
39
+ const configRemoved = removeProviderProfile({ path: globalConfigJsonPath(homeDir), id: selectedProviderId });
40
+ const credentialRemoved = removeProviderCredential({ authStorage, id: selectedProviderId });
41
+ if (!configRemoved && !credentialRemoved) {
42
+ output.write(`Provider not found: ${selectedProviderId}\n`);
43
+ return 1;
44
+ }
45
+
46
+ output.write(`Removed provider: ${provider.label} (${selectedProviderId})\n`);
47
+ return 0;
48
+ }
49
+
50
+ export function listRemovableProviders({ homeDir, authStorage }) {
51
+ const config = readConfigJson(globalConfigJsonPath(homeDir));
52
+ const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
53
+ ? config.providers
54
+ : {};
55
+ const ids = new Set(Object.keys(providers));
56
+ for (const id of safeListAuthProviders(authStorage)) ids.add(id);
57
+ return [...ids].sort((a, b) => getProviderLabel(a).localeCompare(getProviderLabel(b))).map((id) => {
58
+ const sources = [];
59
+ if (Object.prototype.hasOwnProperty.call(providers, id)) sources.push("config");
60
+ if (safeHasAuthProvider(authStorage, id)) sources.push("credential");
61
+ return {
62
+ id,
63
+ label: getProviderLabel(id),
64
+ sources,
65
+ };
66
+ });
67
+ }
68
+
69
+ async function selectProviderToRemove({ removableProviders, input, output, select }) {
70
+ if (!removableProviders.length) return null;
71
+ return await select({
72
+ input,
73
+ output,
74
+ message: "Select provider to remove",
75
+ items: removableProviders.map((provider) => ({
76
+ label: `${provider.label} (${provider.id})${formatSources(provider.sources)}`,
77
+ value: provider.id,
78
+ })),
79
+ });
80
+ }
81
+
82
+ function removeProviderCredential({ authStorage, id }) {
83
+ const existed = safeHasAuthProvider(authStorage, id);
84
+ if (typeof authStorage.remove === "function") authStorage.remove(id);
85
+ return existed;
86
+ }
87
+
88
+ function safeListAuthProviders(authStorage) {
89
+ if (typeof authStorage.list !== "function") return [];
90
+ try {
91
+ const providers = authStorage.list();
92
+ return Array.isArray(providers) ? providers.filter((id) => typeof id === "string" && id) : [];
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
97
+
98
+ function safeHasAuthProvider(authStorage, id) {
99
+ if (typeof authStorage.get === "function") {
100
+ try {
101
+ return authStorage.get(id) != null;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+ return safeListAuthProviders(authStorage).includes(id);
107
+ }
108
+
109
+ function formatSources(sources) {
110
+ if (!sources.length) return "";
111
+ return ` — ${sources.join(" + ")}`;
112
+ }
113
+
114
+ async function confirmRemoval({ input, output, provider }) {
115
+ const answer = String(await readLine({
116
+ input,
117
+ output,
118
+ prompt: `Remove provider "${provider.label}" (${provider.id})? This deletes local config and credentials. [y/N] `,
119
+ }) ?? "").trim().toLowerCase();
120
+ return answer === "y" || answer === "yes";
121
+ }
122
+
123
+ function readLine({ input = process.stdin, output = process.stdout, prompt }) {
124
+ const rl = createInterface({ input, output });
125
+ return new Promise((resolve) => rl.question(prompt, (answer) => {
126
+ rl.close();
127
+ resolve(answer);
128
+ }));
129
+ }
@@ -0,0 +1,149 @@
1
+ import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, closeSync } from "node:fs";
2
+ import { randomUUID, createHash } from "node:crypto";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { getMarchSessionStateDir, normalizeSessionId } from "../state/march-session-state.mjs";
5
+
6
+ export const DEFAULT_SESSION_CONTROLLER_LEASE_TTL_MS = 30_000;
7
+ export const DEFAULT_SESSION_CONTROLLER_HEARTBEAT_MS = 5_000;
8
+
9
+ export class SessionControllerLeaseConflictError extends Error {
10
+ constructor({ sessionId, owner }) {
11
+ super(formatControllerLeaseConflict({ sessionId, owner }));
12
+ this.name = "SessionControllerLeaseConflictError";
13
+ this.code = "SESSION_CONTROLLER_LEASE_CONFLICT";
14
+ this.sessionId = sessionId;
15
+ this.owner = owner;
16
+ }
17
+ }
18
+
19
+ export function createSessionControllerLeaseManager({
20
+ instanceId = randomUUID(),
21
+ pid = process.pid,
22
+ cwd = process.cwd(),
23
+ now = () => Date.now(),
24
+ ttlMs = DEFAULT_SESSION_CONTROLLER_LEASE_TTL_MS,
25
+ heartbeatMs = DEFAULT_SESSION_CONTROLLER_HEARTBEAT_MS,
26
+ } = {}) {
27
+ const ownerBase = { instanceId, pid, cwd: resolve(cwd) };
28
+
29
+ return {
30
+ instanceId,
31
+ acquire(session, options = {}) {
32
+ const target = resolveControllerLeaseTarget(session);
33
+ const path = getSessionControllerLeasePath(target);
34
+ const lease = writeLease({ path, target, ownerBase, now, ttlMs, force: Boolean(options.force) });
35
+ const heartbeat = heartbeatMs > 0 ? setInterval(() => {
36
+ try { refreshLease({ path, lease, now, ttlMs }); } catch {}
37
+ }, heartbeatMs) : null;
38
+ heartbeat?.unref?.();
39
+ return {
40
+ ...lease,
41
+ path,
42
+ target,
43
+ assertOwned() {
44
+ const current = readLease(path);
45
+ if (!current || current.owner?.instanceId !== lease.owner.instanceId || current.token !== lease.token || isExpired(current, now())) {
46
+ throw new SessionControllerLeaseConflictError({ sessionId: target.sessionId, owner: current?.owner ?? null });
47
+ }
48
+ },
49
+ release() {
50
+ if (heartbeat) clearInterval(heartbeat);
51
+ releaseLease({ path, lease });
52
+ },
53
+ };
54
+ },
55
+ };
56
+ }
57
+
58
+ export function getSessionControllerLeasePath({ sessionId, sessionPath = null, projectMarchDir = null }) {
59
+ if (sessionPath) {
60
+ const identity = resolve(sessionPath);
61
+ const key = createHash("sha256").update(identity).digest("hex").slice(0, 32);
62
+ return join(dirname(identity), ".march-controller-leases", `${key}.json`);
63
+ }
64
+ if (!projectMarchDir || !sessionId) throw new Error("session controller lease requires a session path or project March dir plus session id");
65
+ return join(getMarchSessionStateDir(projectMarchDir, sessionId), "controller-lease.json");
66
+ }
67
+
68
+ export function resolveControllerLeaseTarget({ sessionId, sessionPath = null, projectMarchDir = null }) {
69
+ const id = normalizeSessionId(sessionId);
70
+ return {
71
+ sessionId: id,
72
+ sessionPath: sessionPath ? resolve(sessionPath) : null,
73
+ projectMarchDir: projectMarchDir ? resolve(projectMarchDir) : null,
74
+ };
75
+ }
76
+
77
+ function writeLease({ path, target, ownerBase, now, ttlMs, force }) {
78
+ mkdirSync(dirname(path), { recursive: true });
79
+ const current = readLease(path);
80
+ if (current && !force && !isExpired(current, now()) && current.owner?.instanceId !== ownerBase.instanceId) {
81
+ throw new SessionControllerLeaseConflictError({ sessionId: target.sessionId, owner: current.owner });
82
+ }
83
+ const lease = createLease({ target, ownerBase, now, ttlMs });
84
+ if (force || current) {
85
+ writeFileSync(path, JSON.stringify(lease, null, 2), "utf8");
86
+ return lease;
87
+ }
88
+ let fd = null;
89
+ try {
90
+ fd = openSync(path, "wx");
91
+ writeFileSync(fd, JSON.stringify(lease, null, 2), "utf8");
92
+ return lease;
93
+ } catch {
94
+ const raced = readLease(path);
95
+ if (raced && !isExpired(raced, now())) throw new SessionControllerLeaseConflictError({ sessionId: target.sessionId, owner: raced.owner });
96
+ try { unlinkSync(path); } catch {}
97
+ writeFileSync(path, JSON.stringify(lease, null, 2), "utf8");
98
+ return lease;
99
+ } finally {
100
+ if (fd !== null) closeSync(fd);
101
+ }
102
+ }
103
+
104
+ function createLease({ target, ownerBase, now, ttlMs }) {
105
+ const acquiredAtMs = now();
106
+ return {
107
+ version: 1,
108
+ token: randomUUID(),
109
+ sessionId: target.sessionId,
110
+ sessionPath: target.sessionPath,
111
+ owner: ownerBase,
112
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
113
+ heartbeatAt: new Date(acquiredAtMs).toISOString(),
114
+ expiresAt: new Date(acquiredAtMs + ttlMs).toISOString(),
115
+ };
116
+ }
117
+
118
+ function refreshLease({ path, lease, now, ttlMs }) {
119
+ const current = readLease(path);
120
+ if (!current || current.token !== lease.token || current.owner?.instanceId !== lease.owner.instanceId) return;
121
+ const heartbeatAtMs = now();
122
+ writeFileSync(path, JSON.stringify({
123
+ ...current,
124
+ heartbeatAt: new Date(heartbeatAtMs).toISOString(),
125
+ expiresAt: new Date(heartbeatAtMs + ttlMs).toISOString(),
126
+ }, null, 2), "utf8");
127
+ }
128
+
129
+ function releaseLease({ path, lease }) {
130
+ const current = readLease(path);
131
+ if (!current || current.token !== lease.token || current.owner?.instanceId !== lease.owner.instanceId) return;
132
+ try { unlinkSync(path); } catch {}
133
+ }
134
+
135
+ function readLease(path) {
136
+ if (!existsSync(path)) return null;
137
+ try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
138
+ }
139
+
140
+ function isExpired(lease, nowMs) {
141
+ return Date.parse(lease.expiresAt ?? 0) <= nowMs;
142
+ }
143
+
144
+ function formatControllerLeaseConflict({ sessionId, owner }) {
145
+ const parts = [`Session "${sessionId}" is already controlled by another March instance.`];
146
+ if (owner?.cwd) parts.push(`cwd: ${owner.cwd}`);
147
+ if (owner?.pid) parts.push(`pid: ${owner.pid}`);
148
+ return parts.join(" ");
149
+ }
@@ -45,7 +45,7 @@ export function saveMarchSessionState({ projectMarchDir, sessionId, engine, back
45
45
  export function saveMarchSessionStateValue({ projectMarchDir, sessionId, state }) {
46
46
  if (!sessionId) throw new Error("March session id is required");
47
47
  const existing = loadMarchSessionState({ projectMarchDir, sessionId })?.state ?? null;
48
- const nextState = normalizeMarchSessionStateForSave({ ...existing, ...state, renderTimeline: state.renderTimeline ?? existing?.renderTimeline });
48
+ const nextState = normalizeMarchSessionStateForSave({ ...existing, ...state });
49
49
  validateMarchSessionState(nextState);
50
50
  const dir = getMarchSessionStateDir(projectMarchDir, sessionId);
51
51
  mkdirSync(dir, { recursive: true });
@@ -135,22 +135,12 @@ function validateMarchSessionState(state) {
135
135
  function isValidMarchSessionState(state) {
136
136
  return state?.version === MARCH_SESSION_STATE_VERSION
137
137
  && Boolean(state.cwd)
138
- && Array.isArray(state.turns)
139
- && Array.isArray(state.renderTimeline);
138
+ && Array.isArray(state.turns);
140
139
  }
141
140
 
142
141
  function normalizeMarchSessionStateForSave(state) {
143
- return {
144
- ...state,
145
- renderTimeline: normalizePersistedRenderTimeline(state.renderTimeline),
146
- };
147
- }
148
-
149
- function normalizePersistedRenderTimeline(events) {
150
- if (!Array.isArray(events)) return [];
151
- return events
152
- .filter((event) => typeof event?.method === "string" && Array.isArray(event.args))
153
- .map((event) => ({ method: event.method, args: event.args, at: event.at ?? null }));
142
+ const { renderTimeline: _renderTimeline, renderTimelineUpdatedAt: _renderTimelineUpdatedAt, ...coreState } = state ?? {};
143
+ return coreState;
154
144
  }
155
145
 
156
146
  function normalizeSessionRef(sessionRef) {