local-operator-ui 0.30.3 → 0.30.4

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.
Files changed (2) hide show
  1. package/out/main/index.js +58 -3
  2. package/package.json +1 -1
package/out/main/index.js CHANGED
@@ -5773,6 +5773,16 @@ class DaemonStateMachine {
5773
5773
  this.state = "attached";
5774
5774
  this.detail = this.identity ? `Connected to the daemon on ${this.identity.url} (pid ${this.identity.pid}, v${this.identity.version}).` : "Connected to the Local Operator daemon.";
5775
5775
  break;
5776
+ case "reanchored":
5777
+ this.identity = observation.identity;
5778
+ this.failures = 0;
5779
+ this.unanswered = 0;
5780
+ this.capabilityStatus = null;
5781
+ this.detachedSince = null;
5782
+ this.backoffMs = REATTACH_BACKOFF_MS;
5783
+ this.state = "attached";
5784
+ this.detail = observation.detail;
5785
+ break;
5776
5786
  case "capability":
5777
5787
  this.capabilityStatus = observation.status;
5778
5788
  this.detail = `${observation.detail} The daemon is running.`;
@@ -8876,8 +8886,16 @@ class BackendServiceManager {
8876
8886
  const probe = await probeIdentity(this.backendUrl, expected, {
8877
8887
  timeoutMs: PROBE_TIMEOUT_MS
8878
8888
  });
8879
- const answersThisApp = probe.outcome === "identified" && probe.identity.pid === this.daemonState.snapshot().pid;
8880
- const answeredAnotherProcess = probe.outcome === "identity-mismatch" || probe.outcome === "not-a-daemon" || probe.outcome === "identified";
8889
+ const attachedPid = this.daemonState.snapshot().pid;
8890
+ const reanchored = probe.outcome === "identity-mismatch" && attachedPid !== null && probe.identity.pid === attachedPid && this.holdsLiveChildProcess(attachedPid) ? (
8891
+ // Spread rather than field by field: this value IS `probe.identity` plus the
8892
+ // address. A field added to `DaemonIdentity` later must not have to be
8893
+ // remembered in this one literal, where it would silently take the widened
8894
+ // type's `undefined` instead of the answering value.
8895
+ { ...probe.identity, url: this.backendUrl }
8896
+ ) : null;
8897
+ const answersThisApp = reanchored !== null || probe.outcome === "identified" && probe.identity.pid === this.daemonState.snapshot().pid;
8898
+ const answeredAnotherProcess = reanchored === null && (probe.outcome === "identity-mismatch" || probe.outcome === "not-a-daemon" || probe.outcome === "identified");
8881
8899
  if (answersThisApp) {
8882
8900
  this.daemonState.setPairing(DAEMON_PAIRED);
8883
8901
  } else if (answeredAnotherProcess) {
@@ -8893,6 +8911,13 @@ class BackendServiceManager {
8893
8911
  detail: "The answering daemon's PID no longer matches the attachment."
8894
8912
  };
8895
8913
  case "identity-mismatch":
8914
+ if (reanchored) {
8915
+ return {
8916
+ kind: "reanchored",
8917
+ identity: reanchored,
8918
+ detail: `Connected to the daemon on ${this.backendUrl} (pid ${reanchored.pid}, v${reanchored.version}). It reloaded in place and kept the same process.`
8919
+ };
8920
+ }
8896
8921
  return {
8897
8922
  kind: "contradicted",
8898
8923
  detail: `Another process is answering at ${this.backendUrl} (${probe.detail})`
@@ -9019,7 +9044,7 @@ class BackendServiceManager {
9019
9044
  try {
9020
9045
  if (this.daemonState.expectedInstanceId()) {
9021
9046
  const observation = await this.probeAttachedDaemon();
9022
- if (observation.kind === "identified") {
9047
+ if (observation.kind === "identified" || observation.kind === "reanchored") {
9023
9048
  this.daemonState.observe(observation);
9024
9049
  this.nextRecoveryAt = 0;
9025
9050
  this.notifyStatus();
@@ -9068,6 +9093,36 @@ class BackendServiceManager {
9068
9093
  getOwnedPid() {
9069
9094
  return this.process?.pid ?? null;
9070
9095
  }
9096
+ /**
9097
+ * Whether `pid` is the process this app spawned and that process is still live.
9098
+ *
9099
+ * The three clauses are one statement - "this is my child, and it has not
9100
+ * exited" - and each is load-bearing:
9101
+ *
9102
+ * - the handle is the proof of identity. A `ChildProcess` is only ever built by
9103
+ * this manager's own `spawn()`, so its `pid` cannot be a number read off a
9104
+ * record that something else wrote, and it cannot be recycled: the OS does not
9105
+ * reuse a pid while the process it names is alive, and a reused one means THIS
9106
+ * child exited - which the `exitCode` clause reads. That reading is Node's
9107
+ * RECORDED exit, set when the process event arrives rather than when the process
9108
+ * dies, so a pid recycled inside the window before it is recorded would pass
9109
+ * here. It is the same window the recovery guard accepts, and the caller that
9110
+ * matters (the reload re-anchor) needs the answering pid to be bound to this
9111
+ * app's own port as well, which is why this is stated rather than hedged.
9112
+ * - `exitCode === null && signalCode == null` is the same liveness spelling
9113
+ * `stop()` and the recovery guard act on (see `ownedPid`'s note), so a process
9114
+ * this app has already reaped is never treated as live by one caller and dead
9115
+ * by another;
9116
+ * - a `null` pid is not a match. A manager that has never spawned anything owns
9117
+ * nothing, and `undefined === undefined` must not read as ownership.
9118
+ *
9119
+ * Used by the reload re-anchor in `probeAttachedDaemon`, which is the one place
9120
+ * this app decides that a changed `instance_id` belongs to the process it
9121
+ * already holds rather than to a successor.
9122
+ */
9123
+ holdsLiveChildProcess(pid) {
9124
+ return pid !== null && this.process !== null && this.process.pid === pid && this.process.exitCode === null && this.process.signalCode == null;
9125
+ }
9071
9126
  /**
9072
9127
  * The install SERVING this app, read from the serving process's own record.
9073
9128
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-operator-ui",
3
- "version": "0.30.3",
3
+ "version": "0.30.4",
4
4
  "productName": "Local Operator",
5
5
  "description": "User interface for AI agent assistants that run Python code safely on your device through an intuitive chat interface. Supports local and cloud LLM models with built-in security features.",
6
6
  "main": "./out/main/index.js",