Lox Nexus

Starting, failing and degrading

Read this when your device can be unreachable or a login can be wrong: which state to report, and what Nexus does about it.

There are three different ways to say "something is wrong", and they mean different things.

Situation What to do What happens
The user must fix something (wrong credentials, a missing device) Reject start() with a readable message The instance goes to error. Nexus does not restart it.
The device is unreachable right now, but may come back Resolve start(), call this.status.set("degraded", "inverter unreachable"), keep retrying The instance shows as degraded with your message.
The device is back this.status.set("running") Back to normal.

Status messages, errors and log lines are read by people and by AI agents connected over MCP. Never put a password or token in them: Nexus replaces the stored secrets it knows with ‹secret› before an agent sees them (as they are, JSON-escaped, URL-encoded or whole as base64), but not ones it doesn't know (a session token your plugin fetched), ones shorter than 6 characters, nor any other transformation of them.

private async poll() {
  try {
    this.latest = await this.api.read()
    if (this.unreachable) {
      this.unreachable = false
      this.log.info("reading the inverter again")
      this.status.set("running")
    }
    this.emitter.emit("data", this.latest)
  } catch (error) {
    if (!this.unreachable) {
      this.unreachable = true
      this.log.warn("can't read the inverter", { problem: String(error) })
      this.status.set("degraded", `inverter unreachable: ${String(error)}`)
    }
  }
}

Report on changes as above, not on every failed poll, or you flood the log. Do not await slow first requests in start() if the plugin works without them; start them in the background (void this.poller.poll()) so a hanging device does not hold the start up.

A failure of one sensor, as opposed to the whole device, is reported with fail(), see Sensors.

Restarts. Nexus restarts a worker that crashed, ran out of memory (128 MB heap, or the manifest's heapMb), took longer than 30 seconds to start, or stopped answering its pings (sent every 10 seconds; the worker is terminated after three go unanswered, so after roughly 30 to 40 seconds of a busy loop). The delay before a restart doubles each time: 1 s, 2 s, 4 s and so on up to 5 minutes. A run that stays up for 60 seconds resets the delay. After a restart your plugin starts from step 1 of the lifecycle.