Implement configuration, runtime, frontend, and task governance closure

This commit is contained in:
2026-08-21 16:51:42 +08:00
parent b1b5a3cc8a
commit 63c0408ce7
92 changed files with 343619 additions and 172 deletions

View File

@@ -2,6 +2,10 @@ import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
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 { preflightHalConfiguration } from "../workspace/hal-configuration.mjs";
import { DEVICE_COMMANDS, VirtualDevices } from "./virtual-devices.mjs";
import { PythonBridge } from "./python-bridge.mjs";
const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
@@ -11,7 +15,10 @@ const OPERATION_KINDS = new Set([
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP",
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE",
"TOOL_CHANGE", "PROBE_CONFIG", "HAL_COMMAND", "HAL_SET",
"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",
"PYTHON_IMPORT", "PYTHON_CALL", "PYTHON_CAPABILITY",
]);
function text(value) {
@@ -52,6 +59,10 @@ export class SimSession {
this.created = false;
this.timeline = new ReplayTimeline();
this.operations = new Map();
this.devices = new VirtualDevices();
this.python = new PythonBridge();
this.configurationState = null;
this.halState = null;
call(core, "sim_protocol_reset");
if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []);
}
@@ -97,9 +108,27 @@ export class SimSession {
tick: this.tick,
created: this.created,
protocol_seq: this.timeline.events.cursor,
devices: this.devices.export(),
configuration: this.configurationState,
hal: this.halState,
python: this.python.exportState(),
};
}
workspacePackageState() {
const value = {
context_id: this.contextId,
session_id: this.sessionId,
configuration: this.configurationState,
hal: this.halState,
devices: this.devices.export(),
python: this.python.exportState(),
tick: this.tick,
event_cursor: this.timeline.events.cursor,
};
return { ...value, state_hash: fnv1a64(value) };
}
createCheckpoint() {
const latest = this.timeline.snapshots.latest();
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
@@ -119,15 +148,29 @@ export class SimSession {
}
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
const replayPython = new PythonBridge();
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
const result = recoverSimulation({
checkpoint,
contextId: this.contextId,
events,
reconcile,
apply: (state, event) => ({ ...state, tick: event.tick }),
apply: (state, event) => {
if (DEVICE_COMMANDS.has(event.kind)) replayDevices.applyEvent(event.kind, event.payload);
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 (result.status === "recovered" || result.status === "aborted") {
this.tick = result.simulation_tick;
this.devices = new VirtualDevices({ state: result.state?.devices ?? checkpoint?.state?.devices });
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.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);
@@ -198,6 +241,17 @@ export class SimSession {
return response;
};
const deviceResponse = (kind, payload) => {
const result = this.devices.command(kind, payload ?? {});
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
return finish(this.envelope(responseKind, input, {
...result,
command: kind,
operation_id: input.operation_id ?? null,
simulation_tick: this.tick,
}), result.status === "verified" ? "verified" : "failed");
};
switch (input.kind) {
case "HELLO":
return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified");
@@ -211,6 +265,41 @@ export class SimSession {
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");
}
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);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "hal_preflight",
message_key: "hal.preflight_blocked",
retryable: false,
preflight,
}), "failed");
}
this.halState = {
status: preflight.status,
record_count: preflight.record_count,
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");
}
@@ -221,11 +310,30 @@ export class SimSession {
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 }));
}
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":
return deviceResponse(input.kind, input.payload);
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");
}
case "PYTHON_CALL": {
const result = this.python.invoke(input.payload?.module, input.payload?.symbol, input.payload?.args ?? []);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "PYTHON_CAPABILITY": {
const result = this.python.checkCapability(input.payload?.capability);
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() }));
case "CHECKPOINT": {