om-memory-system 4.4.0-next.65 → 4.4.0-next.69

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.
@@ -1,9 +1,12 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { linkSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { existsSync, linkSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { launcherPath } from "./runtime-record.js";
8
+ import { isOlderVersion } from "./version-compare.js";
9
+ import { negotiateOwner, readWebVersion } from "./web-handover.js";
7
10
  const HEALTH_PROBE_MS = 1_000;
8
11
  const POLL_INTERVAL_MS = 250;
9
12
  /** A lock older than this is stale. Also the longest a background start holds it. */
@@ -106,6 +109,13 @@ function processAlive(pid) {
106
109
  return error.code === "EPERM";
107
110
  }
108
111
  }
112
+ /** Tests set the switch, so they never start the developer's real recorded copy. */
113
+ function existingLauncher() {
114
+ if (process.env.OMMS_DISABLE_RUNTIME_RECORD === "1")
115
+ return null;
116
+ const file = launcherPath(join(homedir(), ".omms"));
117
+ return existsSync(file) ? file : null;
118
+ }
109
119
  function productionDefaults() {
110
120
  return {
111
121
  fetch: globalThis.fetch,
@@ -115,6 +125,7 @@ function productionDefaults() {
115
125
  resolveRuntime: async () => (await import("./web-autostart.js")).resolveWebRuntime(),
116
126
  // From dist/services/ to the CLI entry point.
117
127
  cliScript: fileURLToPath(new URL("../cli/index.js", import.meta.url)),
128
+ launcherScript: existingLauncher(),
118
129
  lockPath: startLockPath(),
119
130
  lockFs: nodeLockFs,
120
131
  pidAlive: processAlive,
@@ -211,7 +222,12 @@ export async function ensureWebApp(options) {
211
222
  let held = false;
212
223
  try {
213
224
  const first = await probe(settings.baseUrl, probeMs(), deps);
214
- if (first === "omms")
225
+ const { replaceOlder } = options;
226
+ // An OMMS web app that is older than the newest copy is replaced; any other is used.
227
+ const replacing = first === "omms" &&
228
+ replaceOlder !== undefined &&
229
+ isOlderVersion((await readWebVersion(deps.fetch, settings.baseUrl, replaceOlder.headers)) ?? "", replaceOlder.version);
230
+ if (first === "omms" && !replacing)
215
231
  return "running";
216
232
  if (first === "other") {
217
233
  await logCode("port-busy");
@@ -220,11 +236,27 @@ export async function ensureWebApp(options) {
220
236
  const runtime = await deps.resolveRuntime();
221
237
  if (!runtime) {
222
238
  await logCode("no-runtime");
223
- return "no-runtime";
239
+ return replacing ? "running" : "no-runtime";
224
240
  }
225
241
  held = takeStartLock(deps);
242
+ // The caller that holds the lock replaces the web app; the others use it.
226
243
  if (!held)
227
- return await waitForOther(settings.baseUrl, wait, remaining, deps);
244
+ return replacing ? "running" : await waitForOther(settings.baseUrl, wait, remaining, deps);
245
+ if (replacing && replaceOlder) {
246
+ const outcome = await negotiateOwner({
247
+ fetchFn: deps.fetch,
248
+ sleep: deps.sleep,
249
+ url: settings.baseUrl,
250
+ port: Number(new URL(settings.baseUrl).port) || 80,
251
+ headers: replaceOlder.headers,
252
+ version: replaceOlder.version,
253
+ });
254
+ if (outcome.kind !== "handed") {
255
+ if (outcome.kind === "stuck")
256
+ await logCode("step-aside-failed");
257
+ return "running";
258
+ }
259
+ }
228
260
  // Another caller may have finished its start before this one took the lock.
229
261
  const second = await probe(settings.baseUrl, probeMs(), deps);
230
262
  if (second === "omms")
@@ -233,7 +265,7 @@ export async function ensureWebApp(options) {
233
265
  await logCode("port-busy");
234
266
  return "port-busy";
235
267
  }
236
- const child = deps.spawn(runtime, [deps.cliScript, "web"], {
268
+ const child = deps.spawn(runtime, [deps.launcherScript ?? deps.cliScript, "web"], {
237
269
  detached: true,
238
270
  stdio: "ignore",
239
271
  cwd: homedir(),
@@ -0,0 +1,22 @@
1
+ export declare const POLL_MS = 250;
2
+ export declare const POLL_LIMIT = 40;
3
+ /** The version an OMMS web app reports on `url`, or null when none answers with one. */
4
+ export declare function readWebVersion(fetchFn: typeof fetch, url: string, headers: Record<string, string>): Promise<string | null>;
5
+ export type Handover = {
6
+ kind: "none";
7
+ } | {
8
+ kind: "handed";
9
+ } | {
10
+ kind: "stuck" | "same" | "newer";
11
+ owner: string;
12
+ };
13
+ export interface HandoverContext {
14
+ fetchFn: typeof fetch;
15
+ sleep: (ms: number) => Promise<void>;
16
+ url: string;
17
+ port: number;
18
+ headers: Record<string, string>;
19
+ version: string;
20
+ }
21
+ /** Ask an older OMMS web app on the configured port to step aside, and wait for the port. */
22
+ export declare function negotiateOwner(ctx: HandoverContext): Promise<Handover>;
@@ -0,0 +1,62 @@
1
+ import { compareVersions } from "./version-compare.js";
2
+ // The version handover shared by `web install` and the host start: ask an older
3
+ // OMMS web app to step aside, then wait for its port. It imports no host adapter,
4
+ // no store, and no embedding model.
5
+ export const POLL_MS = 250;
6
+ export const POLL_LIMIT = 40;
7
+ const REQUEST_TIMEOUT_MS = 2_000;
8
+ /** The version an OMMS web app reports on `url`, or null when none answers with one. */
9
+ export async function readWebVersion(fetchFn, url, headers) {
10
+ try {
11
+ const response = await fetchFn(`${url}/api/settings/version`, {
12
+ headers,
13
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
14
+ });
15
+ if (!response.ok)
16
+ return null;
17
+ const body = (await response.json());
18
+ return typeof body.running === "string" ? body.running : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ async function portAnswers(fetchFn, url) {
25
+ try {
26
+ await fetchFn(`${url}/api/health`, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ /** Ask an older OMMS web app on the configured port to step aside, and wait for the port. */
34
+ export async function negotiateOwner(ctx) {
35
+ const owner = await readWebVersion(ctx.fetchFn, ctx.url, ctx.headers);
36
+ const order = owner === null ? null : compareVersions(owner, ctx.version);
37
+ // A version that cannot be placed is never stopped.
38
+ if (owner === null || order === null)
39
+ return { kind: "none" };
40
+ if (order === 0)
41
+ return { kind: "same", owner };
42
+ if (order > 0)
43
+ return { kind: "newer", owner };
44
+ try {
45
+ const reply = await ctx.fetchFn(`${ctx.url}/api/web/step-aside`, {
46
+ method: "POST",
47
+ headers: { ...ctx.headers, "content-type": "application/json" },
48
+ body: JSON.stringify({ version: ctx.version }),
49
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
50
+ });
51
+ if (reply.status !== 202)
52
+ return { kind: "stuck", owner };
53
+ }
54
+ catch {
55
+ return { kind: "stuck", owner };
56
+ }
57
+ // If the old app is slow to stop, the login item still takes the port later.
58
+ for (let i = 0; i < POLL_LIMIT && (await portAnswers(ctx.fetchFn, ctx.url)); i++) {
59
+ await ctx.sleep(POLL_MS);
60
+ }
61
+ return { kind: "handed" };
62
+ }
@@ -802,17 +802,18 @@ export class WebServer {
802
802
  return this.handleStepAside(remoteAddress, body);
803
803
  }
804
804
  if (path === "/api/settings/version" && method === "GET") {
805
- const [{ packageVersion }, { globalCommandVersion }] = await Promise.all([
805
+ const [{ packageVersion }, { globalCommandVersion, globalRelation }] = await Promise.all([
806
806
  import("./package-version.js"),
807
807
  import("./global-version.js"),
808
808
  ]);
809
809
  const running = packageVersion();
810
- const global = await globalCommandVersion();
810
+ const global = globalCommandVersion();
811
811
  return this.jsonResponse({
812
812
  running,
813
813
  global: global.version,
814
814
  globalPath: global.path,
815
815
  mismatch: global.version !== null && global.version !== running,
816
+ relation: globalRelation(running, global.version),
816
817
  });
817
818
  }
818
819
  if (path === "/api/settings/web-autostart" && method === "GET") {