Implement AXIS parity UI and execution baseline

This commit is contained in:
2026-08-23 07:28:37 +08:00
parent 1325504544
commit 06f96f85e5
463 changed files with 84893 additions and 692 deletions

View File

@@ -3,22 +3,40 @@ import { capabilities, validateLimit } from "./capabilities.mjs";
import { mapCoreError } from "./error-map.mjs";
import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs";
import { preflightConfiguration } from "../workspace/configuration.mjs";
import { ConfigurationStartupStateMachine } from "../workspace/configuration.mjs";
import { preflightHalConfiguration } from "../workspace/hal-configuration.mjs";
import { DEVICE_COMMANDS, VirtualDevices } from "./virtual-devices.mjs";
import { PythonBridge } from "./python-bridge.mjs";
import { ProgramRunner } from "./program-runner.mjs";
import { SimTask } from "./sim-task.mjs";
import { SimScheduler, SCHEDULER_PHASES } from "./sim-scheduler.mjs";
import { OperationLedger } from "./operation-ledger.mjs";
import { MotionState } from "./motion-state.mjs";
import { PlcRuntime } from "./plc-runtime.mjs";
import { HalRuntime } from "./hal-runtime.mjs";
import { DeviceIoRuntime } from "./device-io.mjs";
import { IoAckTracker } from "./io-ack.mjs";
import { PythonCallbacks } from "./python-callbacks.mjs";
import { createTrace, validateTrace } from "./trace-export.mjs";
import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs";
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
const OPERATION_KINDS = new Set([
"LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP",
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP",
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE",
"TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
"ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT",
"CONFIGURATION_BATCH",
"PYTHON_IMPORT", "PYTHON_CALL", "PYTHON_CAPABILITY",
"PROGRAM_STEP", "PROGRAM_SEEK", "MDI_EXECUTE", "PLC_CONFIG", "PLC_SET", "PLC_TRIGGER", "FOLLOWING_ERROR", "IO_COMMAND", "IO_ACK", "IO_TIMEOUT",
"REMAP_REGISTER", "REMAP_CALL", "VCP_REGISTER", "VCP_UPDATE", "FILTER_EXEC",
]);
function text(value) {
@@ -59,8 +77,20 @@ export class SimSession {
this.created = false;
this.timeline = new ReplayTimeline();
this.operations = new Map();
this.ledger = new OperationLedger();
this.devices = new VirtualDevices();
this.motion = new MotionState();
this.plc = new PlcRuntime();
this.hal = new HalRuntime();
this.deviceIo = new DeviceIoRuntime();
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: contextId });
this.ioAck = new IoAckTracker();
this.callbacks = new PythonCallbacks();
this.python = new PythonBridge();
this.program = new ProgramRunner(core);
this.authoritative = new AuthoritativeRuntime();
this.task = new SimTask(core, { contextId, sessionId: sessionId || null });
this.scheduler = new SimScheduler(core);
this.configurationState = null;
this.halState = null;
call(core, "sim_protocol_reset");
@@ -102,17 +132,30 @@ export class SimSession {
}
stateForCheckpoint() {
return {
const state = {
context_id: this.contextId,
session_id: this.sessionId,
tick: this.tick,
created: this.created,
protocol_seq: this.timeline.events.cursor,
devices: this.devices.export(),
motion: this.motion.export(),
plc: this.plc.export(),
hal_runtime: this.hal.export(),
device_io: this.deviceIo.export(),
configuration_batch: this.configurationBatch.export(),
io_ack: this.ioAck.export(),
callbacks: this.callbacks.export(),
configuration: this.configurationState,
hal: this.halState,
python: this.python.exportState(),
task: this.task.export(),
scheduler: this.scheduler.export(),
authoritative: this.authoritative.export(),
ledger: this.ledger.export(),
};
if (this.program.loaded) state.program = this.program.exportState();
return state;
}
workspacePackageState() {
@@ -122,10 +165,21 @@ export class SimSession {
configuration: this.configurationState,
hal: this.halState,
devices: this.devices.export(),
motion: this.motion.export(),
plc: this.plc.export(),
hal_runtime: this.hal.export(),
device_io: this.deviceIo.export(),
configuration_batch: this.configurationBatch.export(),
io_ack: this.ioAck.export(),
callbacks: this.callbacks.export(),
python: this.python.exportState(),
tick: this.tick,
event_cursor: this.timeline.events.cursor,
task: this.task.export(),
scheduler: this.scheduler.export(),
authoritative: this.authoritative.export(),
};
if (this.program.loaded) value.program = this.program.exportState();
return { ...value, state_hash: fnv1a64(value) };
}
@@ -147,10 +201,57 @@ export class SimSession {
});
}
exportTrace() {
const latest = this.timeline.snapshots.latest();
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
this.timeline.checkpoint(this.timeline.events.cursor, this.timeline.events.tick, this.stateForCheckpoint(), latest?.snapshot_id ?? null);
}
return createTrace({ contextId: this.contextId, timeline: this.timeline, state: this.stateForCheckpoint(), task: this.task.export(), tick: this.tick });
}
importTrace(trace) {
const validated = validateTrace(trace, { contextId: this.contextId });
this.timeline = validated.timeline;
this.created = Boolean(trace.state?.created);
if (trace.state?.ledger?.entries) this.ledger = new OperationLedger(trace.state.ledger.entries);
if (trace.state?.task) this.task.restore(trace.state.task);
if (trace.state?.scheduler) this.scheduler.restore(trace.state.scheduler);
if (trace.state?.authoritative) this.authoritative.restore(trace.state.authoritative);
if (trace.state?.motion) this.motion.restore(trace.state.motion);
if (trace.state?.plc) this.plc.restore(trace.state.plc);
if (trace.state?.hal_runtime) this.hal.restore(trace.state.hal_runtime);
if (trace.state?.device_io) this.deviceIo.restore(trace.state.device_io);
if (trace.state?.configuration_batch) this.configurationBatch.restore(trace.state.configuration_batch);
if (trace.state?.io_ack) this.ioAck.restore(trace.state.io_ack);
if (trace.state?.callbacks) this.callbacks.restore(trace.state.callbacks);
if (trace.state?.python) this.python.restoreState(trace.state.python);
if (trace.state?.devices) this.devices = new VirtualDevices({ state: trace.state.devices });
this.tick = trace.tick;
return { status: "verified", trace_hash: trace.trace_hash, state_hash: fnv1a64(trace.state), seq: trace.seq, tick: trace.tick };
}
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
const replayMotion = new MotionState();
if (checkpoint?.state?.motion) replayMotion.restore(checkpoint.state.motion);
const replayPlc = new PlcRuntime();
if (checkpoint?.state?.plc) replayPlc.restore(checkpoint.state.plc);
const replayHal = new HalRuntime();
if (checkpoint?.state?.hal_runtime) replayHal.restore(checkpoint.state.hal_runtime);
const replayDeviceIo = new DeviceIoRuntime();
if (checkpoint?.state?.device_io) replayDeviceIo.restore(checkpoint.state.device_io);
const replayConfigurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId });
if (checkpoint?.state?.configuration_batch) replayConfigurationBatch.restore(checkpoint.state.configuration_batch);
const replayIoAck = new IoAckTracker();
if (checkpoint?.state?.io_ack) replayIoAck.restore(checkpoint.state.io_ack);
const replayCallbacks = new PythonCallbacks();
if (checkpoint?.state?.callbacks) replayCallbacks.restore(checkpoint.state.callbacks);
const replayProgram = new ProgramRunner(this.core);
if (checkpoint?.state?.program) replayProgram.restoreState(checkpoint.state.program);
const replayPython = new PythonBridge();
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
const replayAuthoritative = new AuthoritativeRuntime();
if (checkpoint?.state?.authoritative) replayAuthoritative.restore(checkpoint.state.authoritative);
const result = recoverSimulation({
checkpoint,
contextId: this.contextId,
@@ -158,19 +259,132 @@ export class SimSession {
reconcile,
apply: (state, event) => {
if (DEVICE_COMMANDS.has(event.kind)) replayDevices.applyEvent(event.kind, event.payload);
if (event.kind === "HOME") replayMotion.home(event.payload?.axis ?? null);
if (event.kind === "JOG_START") replayMotion.startJog(event.payload);
if (event.kind === "JOG_STOP") replayMotion.stopJog();
if (event.kind === "SET_OVERRIDE") replayMotion.setOverride(event.payload?.feed ?? event.payload?.feed_override, event.payload?.spindle ?? event.payload?.spindle_override);
if (event.kind === "LIMIT_SET") replayMotion.setLimit(event.payload?.axis, event.payload?.active === true);
if (event.kind === "ESTOP") replayMotion.estop(event.payload?.active === true);
if (event.kind === "ESTOP") replayDeviceIo.estop(event.payload?.active === true);
if (event.kind === "IO_RECOVER") replayDeviceIo.interlockReset();
if (event.kind === "SIM_STEP") replayMotion.step(event.tick);
if (event.kind === "SIM_STEP") replayAuthoritative.step(event.tick);
if (event.kind === "PROGRAM_LOAD") replayProgram.load(event.payload?.source ?? event.payload?.text ?? event.payload?.program_source, event.payload?.path ?? null);
if (event.kind === "PROGRAM_RUN") replayProgram.start();
if (event.kind === "PROGRAM_PAUSE") replayProgram.pause();
if (event.kind === "PROGRAM_RESUME") replayProgram.resume();
if (event.kind === "PROGRAM_ABORT") replayProgram.abort();
if (event.kind === "PROGRAM_RESTART") replayProgram.restart();
if (event.kind === "SIM_STEP" && replayProgram.loaded) {
if (replayProgram.status === "paused" && state.task?.program_status === "running") replayProgram.resume();
replayProgram.tick(Number.isSafeInteger(event.payload?.cycles) ? event.payload.cycles : 1);
replayAuthoritative.syncProgram(replayProgram.snapshot());
}
if (event.kind === "PLC_CONFIG") replayPlc.configure(event.payload);
if (event.kind === "PLC_SET") replayPlc.setRegister(event.payload?.register, event.payload?.value);
if (event.kind === "PLC_TRIGGER") replayPlc.trigger();
if (event.kind === "SIM_STEP") for (let replayTick = replayPlc.state.tick + 1; replayTick <= event.tick; replayTick += 1) replayPlc.scan(replayTick);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "configure") replayHal.configure(event.payload);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "thread") replayHal.registerThread(event.payload?.name, event.payload?.period_ticks);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "component") replayHal.registerComponent(event.payload?.instance, event.payload?.type, event.payload?.thread);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "connect") replayHal.connect(event.payload?.source, event.payload?.target);
if (event.kind === "HAL_COMMAND" && event.payload?.action === "failure") replayHal.setFailure(event.payload?.instance, event.payload?.failed === true);
if (event.kind === "HAL_SET" && event.payload?.target === "pin") replayHal.setPin(event.payload?.name, event.payload?.value, event.payload?.type);
if (event.kind === "HAL_SET" && event.payload?.target === "param") replayHal.setParam(event.payload?.name, event.payload?.value, event.payload?.type);
if (event.kind === "SIM_STEP") replayHal.step(event.tick);
if (event.kind === "ENCODER_SET") replayDeviceIo.encoderSet(event.payload?.channel, event.payload);
if (event.kind === "COOLANT_COMMAND") replayDeviceIo.coolantCommand(event.payload);
if (event.kind === "COOLANT_FAULT") replayDeviceIo.coolantFault(event.payload?.fault === true);
if (event.kind === "DOOR_SET") replayDeviceIo.doorSet(event.payload?.open === true);
if (event.kind === "INTERLOCK_SET") replayDeviceIo.interlockSet(event.payload?.input_ok === true);
if (event.kind === "INTERLOCK_RESET") replayDeviceIo.interlockReset();
if (event.kind === "DEVICE_ESTOP") replayDeviceIo.estop(event.payload?.active === true);
if (event.kind === "DEVICE_ACK_ISSUE") replayDeviceIo.issueAck(event.operation_id ?? event.payload?.operation_id, event.payload?.timeout_ticks ?? 10);
if (event.kind === "DEVICE_ACK") replayDeviceIo.ack(event.payload?.operation_id);
if (event.kind === "DEVICE_ACK_ABORT") replayDeviceIo.abortAck(event.payload?.operation_id);
if (event.kind === "SIM_STEP") replayDeviceIo.step(event.tick);
if (event.kind === "CONFIGURATION_BATCH") replayConfigurationBatch.load(event.payload?.entries ?? [], { context_id: this.contextId });
if (event.kind === "IO_COMMAND") replayIoAck.issue(event.payload?.operation_id ?? event.payload?.command_id, event.payload?.kind ?? "IO", event.tick, event.payload?.timeout_ticks ?? 10);
if (event.kind === "IO_COMMAND") replayDeviceIo.issueAck(event.payload?.operation_id ?? event.payload?.command_id, event.payload?.timeout_ticks ?? 10);
if (event.kind === "IO_ACK") replayIoAck.ack(event.payload?.operation_id, event.tick);
if (event.kind === "IO_ACK") replayDeviceIo.ack(event.payload?.operation_id);
if (event.kind === "IO_TIMEOUT") replayIoAck.step(event.tick);
if (event.kind === "SIM_STEP") replayIoAck.step(event.tick);
if (event.kind === "IO_TIMEOUT") replayDeviceIo.ackStep(event.tick);
if (event.kind === "REMAP_REGISTER") replayCallbacks.registerRemap(event.payload?.name, event.payload);
if (event.kind === "VCP_REGISTER") replayCallbacks.registerVcp(event.payload?.name, event.payload?.pins ?? []);
if (event.kind === "FILTER_EXEC") replayCallbacks.filter(event.payload?.name, event.payload?.source ?? "", event.payload?.args ?? []);
if (event.kind === "REMAP_CALL") replayCallbacks.invoke("remap", event.payload?.name, event.payload?.phase ?? "prolog", event.payload?.args ?? []);
if (event.kind === "VCP_UPDATE") replayCallbacks.updateVcp(event.payload?.name, event.payload?.pin, event.payload?.value);
if (event.kind === "PYTHON_IMPORT") replayPython.importModule(event.payload?.module);
if (event.kind === "PYTHON_CALL") replayPython.invoke(event.payload?.module, event.payload?.symbol, event.payload?.args ?? []);
if (event.kind === "PYTHON_CAPABILITY") replayPython.checkCapability(event.payload?.capability);
return { ...state, tick: event.tick, devices: replayDevices.export(), python: replayPython.exportState() };
if (event.kind === "SIM_STEP") replayCallbacks.setTick(event.tick);
if (event.kind === "SIM_STEP") replayPython.setTick(event.tick);
const task = { ...(state.task ?? this.task.export()), seq: event.seq, simulation_tick: event.tick };
delete task.state_hash;
if (event.kind === "ESTOP" && event.payload?.active === true) {
task.state = "ESTOP";
} else if (event.kind === "IO_RECOVER") {
task.state = "READY";
} else if (event.kind === "PROGRAM_PAUSE") {
task.state = "PAUSED";
task.program_status = "paused";
} else if (event.kind === "PROGRAM_RESUME" || event.kind === "PROGRAM_RUN") {
task.state = "RUNNING";
task.program_status = "running";
} else if (event.kind === "PROGRAM_ABORT" || event.kind === "ABORT") {
task.state = "STOPPED";
task.program_status = "aborted";
}
if (replayProgram.loaded) {
const programStatus = replayProgram.status === "completed" ? "completed" : replayProgram.status === "running" ? "running" : replayProgram.status === "paused" ? "paused" : replayProgram.status === "aborted" ? "aborted" : "loaded";
task.current_line = replayProgram.currentLine;
task.program_status = programStatus;
if (programStatus === "completed" && task.state === "RUNNING") task.state = "STOPPED";
}
const schedulerUnsigned = {
schema: "sim_scheduler.v1",
budget: state.scheduler?.budget ?? 1000000,
phase_trace: [...SCHEDULER_PHASES],
tick: event.tick,
};
const scheduler = {
...schedulerUnsigned,
last_cycles: event.kind === "SIM_STEP" && Number.isSafeInteger(event.payload?.cycles) ? event.payload.cycles : (state.scheduler?.last_cycles ?? 0),
state_hash: fnv1a64(schedulerUnsigned),
};
return { ...state, tick: event.tick, task: { ...task, state_hash: fnv1a64(task) }, scheduler, devices: replayDevices.export(), motion: replayMotion.export(), authoritative: replayAuthoritative.export(), plc: replayPlc.export(), hal_runtime: replayHal.export(), device_io: replayDeviceIo.export(), configuration_batch: replayConfigurationBatch.export(), io_ack: replayIoAck.export(), callbacks: replayCallbacks.export(), python: replayPython.exportState(), ...(replayProgram.loaded ? { program: replayProgram.exportState() } : {}) };
},
});
if (result.status === "recovered" || result.status === "aborted") {
this.tick = result.simulation_tick;
if (result.state?.task) this.task.restore(result.state.task);
if (result.state?.scheduler) this.scheduler.restore(result.state.scheduler);
this.authoritative = new AuthoritativeRuntime();
if (result.state?.authoritative) this.authoritative.restore(result.state.authoritative);
this.devices = new VirtualDevices({ state: result.state?.devices ?? checkpoint?.state?.devices });
this.motion = new MotionState();
if (result.state?.motion) this.motion.restore(result.state.motion);
this.authoritative.applyMotion(this.motion);
this.plc = new PlcRuntime();
if (result.state?.plc) this.plc.restore(result.state.plc);
this.hal = new HalRuntime();
if (result.state?.hal_runtime) this.hal.restore(result.state.hal_runtime);
this.deviceIo = new DeviceIoRuntime();
if (result.state?.device_io) this.deviceIo.restore(result.state.device_io);
this.configurationBatch = new ConfigurationBatchRuntime({ context_id: this.contextId });
if (result.state?.configuration_batch) this.configurationBatch.restore(result.state.configuration_batch);
this.ioAck = new IoAckTracker();
if (result.state?.io_ack) this.ioAck.restore(result.state.io_ack);
this.callbacks = new PythonCallbacks();
if (result.state?.callbacks) this.callbacks.restore(result.state.callbacks);
this.configurationState = result.state?.configuration ?? checkpoint?.state?.configuration ?? null;
this.halState = result.state?.hal ?? checkpoint?.state?.hal ?? null;
this.python = new PythonBridge();
if (result.state?.python) this.python.restoreState(result.state.python);
this.program = new ProgramRunner(this.core);
if (result.state?.program) this.program.restoreState(result.state.program);
this.timeline = new ReplayTimeline(checkpoint.timeline);
if (events?.schema === "replay-timeline.v1") {
for (const event of events.events.events.slice(this.timeline.events.cursor)) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
@@ -178,6 +392,11 @@ export class SimSession {
for (const event of events) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
}
this.operations = new Map(result.operations.map((operation) => [operation.operation_id, operation]));
this.ledger = new OperationLedger();
for (const operation of result.operations) {
const entry = { ...operation, phase: operation.phase ?? (operation.status === "verified" ? "committed" : operation.status), side_effect_count: operation.side_effect_count ?? (operation.status === "verified" ? 1 : 0) };
try { this.ledger.importEntry(entry); } catch { /* legacy checkpoint entries remain in operations */ }
}
}
return result;
}
@@ -188,7 +407,10 @@ export class SimSession {
if (input.context_id !== this.contextId) return ["protocol_context", "context_mismatch"];
if (!text(input.kind) || !Number.isSafeInteger(input.seq) || input.seq <= 0) return ["protocol_argument", "invalid_sequence_or_kind"];
if (this.sessionId && input.session_id !== this.sessionId) return ["protocol_session", "session_mismatch"];
if (!this.sessionId && text(input.session_id)) this.sessionId = input.session_id;
if (!this.sessionId && text(input.session_id)) {
this.sessionId = input.session_id;
this.task.bindSession(this.sessionId);
}
return null;
}
@@ -197,6 +419,37 @@ export class SimSession {
if (invalid) return this.error(input ?? {}, invalid[0], invalid[1]);
const result = call(this.core, "sim_protocol_accept", input.schema_version, input.seq, input.kind, input.operation_id ?? "", input.simulation_tick ?? this.tick);
if (result !== 0) return this.coreError(input, result, "sim_protocol_accept");
let ledgerPreparation;
try {
ledgerPreparation = this.ledger.prepare(input);
if (ledgerPreparation.replay && ledgerPreparation.entry?.response) {
const replayTick = Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick ? input.simulation_tick : this.tick;
this.timeline.appendEvent(input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
const replayEventResult = appendCoreEvent(this.core, input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
if (replayEventResult !== 0) return this.error(input, "event_log", "core_event_append_failed");
this.tick = replayTick;
try { this.task.setCommand(input.seq, replayTick); } catch (error) { return this.error(input, "task_state", String(error?.message ?? error)); }
return { ...ledgerPreparation.entry.response, replayed: true, seq: input.seq, ledger: this.ledger.reconcile(input.operation_id) };
}
if (ledgerPreparation.entry) this.ledger.execute(input.operation_id);
} catch (error) {
const ledgerCode = String(error?.message ?? error);
try {
this.task.setCommand(input.seq, input.simulation_tick ?? this.tick);
this.tick = this.task.state.simulation_tick;
} catch { /* protocol has already accepted the command; retain ledger error */ }
try {
this.timeline.appendEvent(input.seq, this.tick, "COMMAND_REJECTED", { code: ledgerCode, operation_id: input.operation_id ?? null });
appendCoreEvent(this.core, input.seq, this.tick, "COMMAND_REJECTED", { code: ledgerCode, operation_id: input.operation_id ?? null });
} catch { /* preserve the original ledger diagnostic */ }
return this.error(input, ledgerCode.startsWith("ledger_") ? ledgerCode : "ledger", ledgerCode);
}
try {
this.task.setCommand(input.seq, input.simulation_tick ?? this.tick);
this.tick = this.task.state.simulation_tick;
} catch (error) {
return this.error(input, "task_state", String(error?.message ?? error));
}
const existingOperation = this.operationState(input);
if (existingOperation?.response) {
if (existingOperation.kind !== input.kind) return this.error(input, "recovery_operation_conflict", "operation_id_conflict");
@@ -237,21 +490,54 @@ export class SimSession {
operation.result_hash = fnv1a64(response.payload ?? {});
operation.response = response;
}
try { this.ledger.verify(input.operation_id, response); this.ledger.commit(input.operation_id, input.seq); } catch (error) {
const ledgerCode = String(error?.message ?? error);
return this.error(input, ledgerCode.startsWith("ledger_") ? ledgerCode : "ledger", ledgerCode);
}
}
return response;
};
const deviceResponse = (kind, payload) => {
const result = this.devices.command(kind, payload ?? {});
try {
if (kind === "ESTOP" && payload?.active === true) this.task.estop(-904);
else if (kind === "IO_RECOVER" && result.status === "verified") this.task.recover();
else if (kind === "TOOL_FAULT" && payload?.fault === true) this.task.fault(-905);
} catch (error) {
return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed");
}
if (kind === "ESTOP") this.deviceIo.estop(payload?.active === true);
if (kind === "IO_RECOVER" && result.status === "verified") this.deviceIo.interlockReset();
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
return finish(this.envelope(responseKind, input, {
...result,
command: kind,
operation_id: input.operation_id ?? null,
...result,
command: kind,
task: this.task.export(),
device_io: this.deviceIo.export(),
motion: this.motion.export(),
operation_id: input.operation_id ?? null,
simulation_tick: this.tick,
}), result.status === "verified" ? "verified" : "failed");
};
const deviceIoResponse = (kind, payload = {}) => {
let result;
if (kind === "ENCODER_SET") result = this.deviceIo.encoderSet(payload.channel, payload);
else if (kind === "COOLANT_COMMAND") result = this.deviceIo.coolantCommand(payload);
else if (kind === "COOLANT_FAULT") result = this.deviceIo.coolantFault(payload.fault === true);
else if (kind === "DOOR_SET") result = this.deviceIo.doorSet(payload.open === true);
else if (kind === "INTERLOCK_SET") result = this.deviceIo.interlockSet(payload.input_ok === true);
else if (kind === "INTERLOCK_RESET") result = this.deviceIo.interlockReset();
else if (kind === "DEVICE_ESTOP") result = this.deviceIo.estop(payload.active === true);
else if (kind === "DEVICE_ACK_ISSUE") result = this.deviceIo.issueAck(input.operation_id ?? payload.operation_id, payload.timeout_ticks ?? 10);
else if (kind === "DEVICE_ACK_ABORT") result = this.deviceIo.abortAck(payload.operation_id);
else if (kind === "DEVICE_ACK") result = this.deviceIo.ack(payload.operation_id);
else result = this.deviceIo.fail("device_io_command_unsupported");
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
return finish(this.envelope(responseKind, input, { ...result, command: kind, device_io: this.deviceIo.export(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
};
switch (input.kind) {
case "HELLO":
return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified");
@@ -261,27 +547,57 @@ export class SimSession {
if (createResult !== 0) return finish(this.coreError(input, createResult, "sim_create"), "failed");
this.created = true;
}
return finish(this.envelope("SESSION_READY", input, { state_hash: fnv1a64({ tick: this.tick }), subscriptions: [] }), "verified");
try { this.task.transition("READY"); } catch (error) { return finish(this.error(input, "task_state", String(error?.message ?? error)), "failed"); }
return finish(this.envelope("SESSION_READY", input, { state_hash: this.task.hash(), task: this.task.export(), subscriptions: [] }), "verified");
case "LOAD_WORKSPACE": {
const rootPath = input.payload?.root_path;
if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed");
if (input.payload?.configuration) {
const preflight = preflightConfiguration(input.payload.configuration);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "configuration_preflight",
message_key: "configuration.preflight_blocked",
retryable: false,
preflight,
}), "failed");
let startupLoaded = false;
const configuration = input.payload?.configuration ?? null;
if (configuration) {
const startupWorkspace = configuration.workspace ?? configuration.files ?? configuration.workspace_files;
if (startupWorkspace) {
const startup = new ConfigurationStartupStateMachine({
workspace: startupWorkspace,
entrypoint: configuration.entrypoint,
variables: configuration.variables ?? {},
maxDepth: configuration.maxDepth ?? 32,
core: this.core,
coreCreated: this.created,
task: this.task,
halManifest: configuration.halManifest ?? configuration.hal_manifest ?? configuration.manifest ?? [],
});
const startupReport = startup.run();
if (startupReport.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "configuration_startup",
message_key: "configuration.startup_blocked",
retryable: false,
startup: startupReport,
}), "failed");
}
this.configurationState = startupReport;
this.created = true;
startupLoaded = true;
} else {
const preflight = preflightConfiguration(input.payload.configuration);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "configuration_preflight",
message_key: "configuration.preflight_blocked",
retryable: false,
preflight,
}), "failed");
}
this.configurationState = {
entrypoint: input.payload.configuration.entrypoint ?? null,
status: preflight.status,
resource_count: preflight.resource_count,
state_hash: fnv1a64(preflight),
};
}
this.configurationState = {
entrypoint: input.payload.configuration.entrypoint ?? null,
status: preflight.status,
resource_count: preflight.resource_count,
state_hash: fnv1a64(preflight),
};
}
if (input.payload?.hal) {
const preflight = preflightHalConfiguration(input.payload.hal);
@@ -300,28 +616,268 @@ export class SimSession {
state_hash: fnv1a64(preflight),
};
}
const loadResult = call(this.core, "sim_load_workspace", rootPath);
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, state_hash: fnv1a64({ tick: this.tick }) })) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
if (configuredKinematics) {
const kinematics = this.authoritative.setKinematics(configuredKinematics);
if (kinematics.status === "blocked") return finish(this.envelope("ERROR", input, { status: "blocked", code: kinematics.code, kinematics: this.authoritative.export() }), "failed");
}
const loadResult = startupLoaded ? 0 : call(this.core, "sim_load_workspace", rootPath);
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, {
status: "verified",
command: input.kind,
state_hash: this.task.hash(),
task: this.task.export(),
...(this.configurationState?.schema === "configuration-startup-runtime.v1" ? { configuration: this.configurationState } : {}),
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
}
case "SIM_STEP": {
const cycles = input.payload?.cycles;
const limit = validateLimit("max_tick_step", cycles);
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
const stepResult = call(this.core, "sim_step", cycles);
if (stepResult !== 0) return finish(this.coreError(input, stepResult, "sim_step"), "failed");
this.tick += cycles;
this.python.setTick(this.tick);
const protocolTick = call(this.core, "sim_protocol_set_tick", this.tick);
if (protocolTick !== 0) return finish(this.error(input, "protocol_tick", "tick_update_failed"), "failed");
const snapshot = this.timeline.checkpoint(input.seq, this.tick, { tick: this.tick, cycles }, this.timeline.snapshots.latest()?.snapshot_id ?? null);
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, state_hash: snapshot.state_hash, snapshot }));
let program_steps = [];
try {
this.scheduler.step(cycles, {
task: () => {
program_steps.push(...this.program.tick(1));
},
tp: (nextTick) => {
if (this.program.loaded) {
const sync = this.authoritative.syncProgram(this.program.snapshot());
if (sync.status === "blocked") throw new Error(sync.code);
}
const planner = this.authoritative.step(nextTick);
if (planner.status === "error") throw new Error(planner.code);
},
kinematics: () => {
if (!["trivkins", "corexy", "tripod", "lineardelta", "rotarydelta"].includes(this.authoritative.state.kinematics.type)) throw new Error("authoritative_kinematics_unsupported");
},
motion: (nextTick) => {
const stepResult = call(this.core, "sim_step", 1);
if (stepResult !== 0) throw new Error(`core_sim_step_${stepResult}`);
this.tick = nextTick;
this.task.setTick(nextTick);
const motionState = this.motion.step(nextTick);
if (motionState.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
if (this.program.loaded) this.authoritative.applyMotion(this.motion);
const halState = this.hal.step(nextTick);
if (halState.status === "error") throw new Error(halState.code);
const deviceIoState = this.deviceIo.step(nextTick);
if (deviceIoState.status === "blocked") throw new Error(deviceIoState.code);
const plcState = this.plc.scan(nextTick);
if (plcState.status === "error") throw new Error(plcState.code);
const ackState = this.ioAck.step(nextTick);
if (ackState.status === "error") throw new Error(ackState.code);
const callbackClock = this.callbacks.setTick(nextTick);
if (callbackClock.status === "blocked") throw new Error(callbackClock.code);
this.python.setTick(nextTick);
const protocolTick = call(this.core, "sim_protocol_set_tick", nextTick);
if (protocolTick !== 0) throw new Error(`protocol_tick_${protocolTick}`);
},
});
} catch (error) {
return finish(this.error(input, "scheduler", String(error?.message ?? error)), "failed");
}
const program = this.program.loaded ? this.program.snapshot() : null;
if (program) {
const status = program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status;
this.task.setProgram(status, program.line);
if (status === "completed" && this.task.state.state === "RUNNING") this.task.transition("STOPPED");
}
const snapshotState = { tick: this.tick, cycles, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export() };
if (program) snapshotState.program = program;
const snapshot = this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, program_steps, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.authoritative.export(), motion: this.motion.export(), plc: this.plc.export(), hal_runtime: this.hal.export(), device_io: this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), ...(program ? { program } : {}), state_hash: snapshot.state_hash, snapshot }));
}
case "PROGRAM_LOAD": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source;
const result = this.program.load(source, input.payload?.path ?? null);
if (result.status === "verified") {
this.authoritative.reset();
this.task.setProgram("loaded", 0);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "PROGRAM_RUN": {
const result = this.program.start();
if (result.status === "verified") {
this.task.setMode("AUTO");
if (this.task.state.state === "STOPPED") this.task.recover();
this.task.transition("RUNNING");
this.task.setProgram("running", this.program.currentLine);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
const result = this.program[action]();
if (result.status === "verified") {
if (action === "pause") this.task.transition("PAUSED");
else if (action === "resume") this.task.transition("RUNNING");
else { this.authoritative.abort(); this.task.transition("STOPPED"); this.task.setProgram("aborted", this.program.currentLine); }
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "PROGRAM_RESTART": {
const result = this.program.restart();
if (result.status === "verified") {
if (["ESTOP", "FAULT"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_restart_faulted"), "failed");
if (this.task.state.state === "STOPPED") this.task.recover();
this.task.setMode("AUTO");
this.authoritative.reset();
this.task.setProgram("loaded", 0);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "PROGRAM_STEP": {
if (this.task.state.state === "READY") {
this.task.setMode("AUTO");
this.task.transition("RUNNING");
}
const result = this.program.step();
if (result.status === "verified") {
this.task.setProgram(result.program_status === "completed" ? "completed" : "paused", result.line);
if (result.program_status === "completed") {
if (this.task.state.state === "RUNNING") this.task.transition("STOPPED");
} else if (this.task.state.state === "RUNNING") this.task.transition("PAUSED");
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "PROGRAM_SEEK": {
const result = this.program.seek(input.payload?.line);
if (result.status === "verified") this.task.setProgram("paused", result.line);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "MDI_EXECUTE": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "mdi_while_program_active"), "failed");
if (this.task.state.state === "STOPPED") this.task.recover();
const result = this.program.executeBlock(input.payload?.block ?? input.payload?.command);
if (result.status === "verified") { this.task.setMode("MDI"); this.task.setProgram("loaded", this.program.currentLine); }
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, {
...result,
task: this.task.export(),
command: input.kind,
operation_id: input.operation_id ?? null,
}), result.status === "verified" ? "verified" : "failed");
}
case "HOME": {
const result = this.motion.home(input.payload?.axis ?? null);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "JOG_START": case "JOG_STOP": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
let result;
if (input.kind === "JOG_START") result = this.motion.startJog(input.payload);
else if (input.kind === "JOG_STOP") result = this.motion.stopJog();
else if (input.kind === "SET_OVERRIDE") result = this.motion.setOverride(input.payload?.feed ?? input.payload?.feed_override, input.payload?.spindle ?? input.payload?.spindle_override);
else result = this.motion.setFollowingError(input.payload?.value ?? input.payload?.following_error);
if (result.status === "error" && result.state?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "SPINDLE_COMMAND": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
case "PROBE_CONFIG": case "LIMIT_SET": case "ESTOP": case "IO_RECOVER":
case "PROBE_CONFIG": case "IO_RECOVER":
return deviceResponse(input.kind, input.payload);
case "LIMIT_SET": {
const result = this.motion.setLimit(input.payload?.axis, input.payload?.active === true);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "PLC_CONFIG": case "PLC_SET": case "PLC_TRIGGER": {
const result = input.kind === "PLC_CONFIG" ? this.plc.configure(input.payload) : input.kind === "PLC_SET" ? this.plc.setRegister(input.payload?.register, input.payload?.value) : this.plc.trigger();
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), plc: this.plc.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "CONFIGURATION_BATCH": {
const result = this.configurationBatch.load(input.payload?.entries, { context_id: this.contextId });
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, configuration_batch: this.configurationBatch.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "HAL_COMMAND": case "HAL_SET": {
const payload = input.payload ?? {};
let result;
if (input.kind === "HAL_SET") {
result = payload.target === "param" ? this.hal.setParam(payload.name, payload.value, payload.type) : this.hal.setPin(payload.name, payload.value, payload.type);
} else if (payload.action === "configure") {
result = this.hal.configure(payload);
} else if (payload.action === "thread") {
result = this.hal.registerThread(payload.name, payload.period_ticks);
} else if (payload.action === "component") {
result = this.hal.registerComponent(payload.instance ?? payload.name, payload.type, payload.thread ?? "servo-thread");
} else if (payload.action === "connect") {
result = this.hal.connect(payload.source, payload.target);
} else if (payload.action === "failure") {
result = this.hal.setFailure(payload.instance, payload.failed === true);
} else if (payload.action === "step") {
result = this.hal.step(payload.tick ?? this.tick);
} else {
result = this.hal.fail("hal_command_unsupported");
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, task: this.task.export(), hal_runtime: this.hal.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "ENCODER_SET": case "COOLANT_COMMAND": case "COOLANT_FAULT": case "DOOR_SET": case "INTERLOCK_SET": case "INTERLOCK_RESET": case "DEVICE_ESTOP": case "DEVICE_ACK_ISSUE": case "DEVICE_ACK": case "DEVICE_ACK_ABORT":
return deviceIoResponse(input.kind, input.payload);
case "IO_COMMAND": case "IO_ACK": case "IO_TIMEOUT": {
let result;
if (input.kind === "IO_COMMAND") {
result = this.ioAck.issue(input.operation_id, input.payload?.kind ?? "IO", this.tick, input.payload?.timeout_ticks ?? 10);
if (result.status === "verified") this.deviceIo.issueAck(input.operation_id, input.payload?.timeout_ticks ?? 10);
} else if (input.kind === "IO_ACK") {
result = this.ioAck.ack(input.payload?.operation_id, this.tick);
this.deviceIo.ack(input.payload?.operation_id);
} else {
const targetTick = this.tick + (input.payload?.ticks ?? 0);
result = this.ioAck.step(targetTick);
this.deviceIo.ackStep(targetTick);
}
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, io_ack: this.ioAck.export(), device_io: this.deviceIo.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "REMAP_REGISTER": case "REMAP_CALL": case "VCP_REGISTER": case "VCP_UPDATE": case "FILTER_EXEC": {
const result = input.kind === "REMAP_REGISTER"
? this.callbacks.registerRemap(input.payload?.name, input.payload)
: input.kind === "REMAP_CALL"
? this.callbacks.invoke("remap", input.payload?.name, input.payload?.phase ?? "prolog", input.payload?.args ?? [])
: input.kind === "VCP_REGISTER"
? this.callbacks.registerVcp(input.payload?.name, input.payload?.pins ?? [])
: input.kind === "VCP_UPDATE"
? this.callbacks.updateVcp(input.payload?.name, input.payload?.pin, input.payload?.value)
: this.callbacks.filter(input.payload?.name, input.payload?.source ?? "", input.payload?.args ?? []);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, callbacks: this.callbacks.export(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
case "ESTOP": {
const motionResult = this.motion.estop(input.payload?.active === true);
const deviceResult = deviceResponse(input.kind, input.payload);
if (motionResult.status === "error") return finish(this.envelope("ERROR", input, { ...motionResult, task: this.task.export() }), "failed");
return deviceResult;
}
case "PYTHON_IMPORT": {
const result = this.python.importModule(input.payload?.module);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
@@ -335,7 +891,7 @@ export class SimSession {
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "SNAPSHOT_REQUEST":
return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, state_hash: fnv1a64({ tick: this.tick }), snapshot: this.timeline.snapshots.latest() }));
return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, task: this.task.export(), state_hash: this.task.hash(), snapshot: this.timeline.snapshots.latest() }));
case "CHECKPOINT": {
try {
const operation = text(input.operation_id) ? this.operations.get(input.operation_id) : null;
@@ -354,9 +910,19 @@ export class SimSession {
const target = input.payload?.operation_id;
const operation = text(target) ? this.operations.get(target) : null;
if (operation && operation.status !== "verified") operation.status = "aborted";
return finish(this.envelope("COMMAND_RESULT", input, { status: operation ? "aborted" : "verified", operation_id: target ?? null }));
let ledgerResult = null;
if (text(target)) {
try { ledgerResult = this.ledger.abort(target); } catch (error) { ledgerResult = { status: "missing", error: String(error?.message ?? error) }; }
}
if (this.task.state.state !== "DESTROYED") {
try { this.task.transition("STOPPED"); } catch { /* target abort can be bookkeeping-only */ }
}
return finish(this.envelope("COMMAND_RESULT", input, { status: ledgerResult?.status === "too_late" ? "too_late" : operation ? "aborted" : "verified", operation_id: target ?? null, ledger: ledgerResult }));
}
case "DESTROY":
this.program.destroy();
this.task.destroy();
this.scheduler.destroy();
call(this.core, "sim_destroy");
this.created = false;
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", destroyed: true, replay: this.timeline.export() }));