diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 607ce71..61ca0f0 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -239,6 +239,9 @@ export function App() { .then(() => dispatch("PROGRAM_RUN", {}, false)) .catch(() => undefined); }; + const clearLoadedPreview = async () => { + await operationController.execute("VISUAL_CLEAR", {}, { state: axisState, gate: false }); + }; const loadCase = async () => { if (programLoading || programRunning) return; setProgramLoading(true); @@ -256,6 +259,7 @@ export function App() { const operation = await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }, { state: axisState, gate: false }); const resultPayload = (operation.response as { payload?: { configuration?: { state?: string } } }).payload; setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); setPlotCleared(false); + await clearLoadedPreview(); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: axisState, gate: false }); setOperationTrace(operationController.exportTrace()); setCommandStatus("PROGRAM_LOAD acknowledged"); @@ -277,6 +281,7 @@ export function App() { const source = normalizeProgramSource(await response.text()); if (!source.trim()) throw new Error("program_asset_empty"); setProgramText(source); setProgramPath(path); setPlotCleared(false); + await clearLoadedPreview(); await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false }); setOperationTrace(operationController.exportTrace()); setCommandStatus("PROGRAM_LOAD acknowledged"); @@ -291,7 +296,7 @@ export function App() { setProgramLoading(true); setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview"); setPlotCleared(false); - void dispatch("PROGRAM_LOAD", { source: programText, path: programPath }) + void clearLoadedPreview().then(() => dispatch("PROGRAM_LOAD", { source: programText, path: programPath })) .then(() => setCaseStatus(`${programPath} loaded through linuxcnc-wasm`)) .catch(() => undefined) .finally(() => setProgramLoading(false)); diff --git a/web/worker/operation-ledger.mjs b/web/worker/operation-ledger.mjs index 2824ea3..be4e3a3 100644 --- a/web/worker/operation-ledger.mjs +++ b/web/worker/operation-ledger.mjs @@ -56,7 +56,13 @@ export class OperationLedger { entry.phase = "verified"; entry.status = response?.payload?.status === "error" || response?.kind === "ERROR" ? "failed" : "verified"; entry.result_hash = fnv1a64(response?.payload ?? {}); - entry.response = clone(response); + const payload = response?.payload; + // High-frequency fixed-tick responses are deltas. Keep only the compact + // operation evidence in the ledger; the full delta is already delivered + // to the client and retained by the frontend trace window. + entry.response = payload?.snapshot_mode === "motion_delta" + ? { ...response, payload: { status: payload.status, snapshot_mode: payload.snapshot_mode, simulation_tick: response.simulation_tick ?? null, program: payload.program ? { current_line: payload.program.current_line, program_status: payload.program.program_status, state_hash: payload.program.state_hash } : null } } + : clone(response); return clone(entry); } diff --git a/web/worker/xyzac-command-ledger.mjs b/web/worker/xyzac-command-ledger.mjs index 321856b..625026c 100644 --- a/web/worker/xyzac-command-ledger.mjs +++ b/web/worker/xyzac-command-ledger.mjs @@ -45,11 +45,13 @@ export class XyzacCommandLedger extends OperationLedger { return { status: "reconcile_required", operation_id: operationId, retry_allowed: false, reconciliation }; } - audit({ includeResponses = true } = {}) { + audit({ includeResponses = true, maxEntries = Number.POSITIVE_INFINITY } = {}) { const exported = super.export(); const pending = this.pending(); - const entries = includeResponses ? exported.entries : exported.entries.map((entry) => { const { response, ...summary } = entry; return summary; }); - const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, pending_count: pending.length }; + const totalEntries = exported.entries.length; + const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? exported.entries.slice(-Math.floor(maxEntries)) : exported.entries; + const entries = includeResponses ? windowed : windowed.map((entry) => { const { response, ...summary } = entry; return summary; }); + const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, entry_count: totalEntries, truncated: entries.length < totalEntries, pending_count: pending.length }; return { ...unsigned, state_hash: fnv1a64(unsigned) }; } } diff --git a/web/worker/xyzac-session.mjs b/web/worker/xyzac-session.mjs index 7ad8cbd..32edbdc 100644 --- a/web/worker/xyzac-session.mjs +++ b/web/worker/xyzac-session.mjs @@ -34,7 +34,7 @@ export class XyzacSession extends SimSession { // every previous response inside the stored response object. const payload = { ...response.payload }; payload.xyzac_session = this.authority(); - payload.xyzac_ledger = this.ledger.audit({ includeResponses: input.kind !== "SIM_STEP" }); + payload.xyzac_ledger = this.ledger.audit({ includeResponses: input.kind !== "SIM_STEP", maxEntries: input.kind === "SIM_STEP" ? 32 : 2048 }); payload.xyzac_scheduler = this.scheduler.export(); return { ...response, payload }; }