@volter/world-runtime 3.0.55 → 3.0.57

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.
@@ -21,6 +21,15 @@ export declare function createConsumer(root: string, name: string, instanceCreat
21
21
  }): WorldConsumer;
22
22
  export declare function refreshConsumer(root: string, name: string, record: WorldConsumer): void;
23
23
  export declare function releaseConsumer(root: string, name: string, record: WorldConsumer): void;
24
+ export type ConsumerProcessState = {
25
+ state: 'present' | 'absent';
26
+ } | {
27
+ state: 'unknown';
28
+ reason: string;
29
+ };
30
+ /** Signal delivery cannot prove existence. Only a successful signal-0 probe or ESRCH
31
+ * answers it; permission and registry errors retain uncertainty, never cleanup authority. */
32
+ export declare function consumerProcessState(pid: number): ConsumerProcessState;
24
33
  /** Inspection never deletes records or interprets incomplete coverage as no consumers. */
25
34
  export declare function worldConsumers(root: string, name: string, instanceCreatedAt?: string): {
26
35
  consumers: (WorldConsumer & {
@@ -80,15 +80,18 @@ export function releaseConsumer(root, name, record) {
80
80
  rmSync(join(dir, `${record.id}.json`), { force: true });
81
81
  }
82
82
  }
83
- function alive(pid) {
84
- if (!pid)
85
- return false;
83
+ /** Signal delivery cannot prove existence. Only a successful signal-0 probe or ESRCH
84
+ * answers it; permission and registry errors retain uncertainty, never cleanup authority. */
85
+ export function consumerProcessState(pid) {
86
86
  try {
87
87
  process.kill(pid, 0);
88
- return true;
88
+ return { state: 'present' };
89
89
  }
90
90
  catch (error) {
91
- return error.code !== 'ESRCH';
91
+ const cause = error;
92
+ if (cause?.code === 'ESRCH')
93
+ return { state: 'absent' };
94
+ return { state: 'unknown', reason: cause?.code ?? cause?.message ?? String(error) };
92
95
  }
93
96
  }
94
97
  /** Inspection never deletes records or interprets incomplete coverage as no consumers. */
@@ -131,10 +134,18 @@ export function worldConsumers(root, name, instanceCreatedAt) {
131
134
  checkedOwner(r.owner);
132
135
  const age = Date.now() - Date.parse(r.heartbeatAt);
133
136
  const sameInstance = instanceCreatedAt === r.instanceCreatedAt;
134
- const state = r.retirementUncertain || r.hostname !== hostname() || !sameInstance ? 'uncertain'
135
- : !alive(r.runnerPid) && r.consumerPid === undefined ? 'uncertain'
136
- : !alive(r.runnerPid) && !alive(r.consumerPid) ? (r.completionRequired ? 'uncertain' : 'exited')
137
- : alive(r.runnerPid) && age >= -STALE_MS && age <= STALE_MS ? 'active' : 'uncertain';
137
+ // Never interpret another host's numeric PIDs as processes on this host.
138
+ const local = r.hostname === hostname() && sameInstance;
139
+ const runner = local ? consumerProcessState(r.runnerPid) : undefined;
140
+ const command = local && r.consumerPid !== undefined ? consumerProcessState(r.consumerPid) : undefined;
141
+ if (runner?.state === 'unknown')
142
+ warnings.push(`Consumer ${r.id}: runner ${r.runnerPid} existence unknown (${runner.reason}).`);
143
+ if (command?.state === 'unknown')
144
+ warnings.push(`Consumer ${r.id}: command ${r.consumerPid} existence unknown (${command.reason}).`);
145
+ const state = r.retirementUncertain || !local || runner?.state === 'unknown' || command?.state === 'unknown' ? 'uncertain'
146
+ : runner?.state === 'absent' && r.consumerPid === undefined ? 'uncertain'
147
+ : runner?.state === 'absent' && command?.state === 'absent' ? (r.completionRequired ? 'uncertain' : 'exited')
148
+ : runner?.state === 'present' && age >= -STALE_MS && age <= STALE_MS ? 'active' : 'uncertain';
138
149
  // Explicit fields only: malformed records must never leak arbitrary metadata into reports.
139
150
  consumers.push({ id: r.id, instanceCreatedAt: r.instanceCreatedAt, kind: r.kind, owner: r.owner,
140
151
  uid: r.uid, hostname: r.hostname, runnerPid: r.runnerPid, consumerPid: r.consumerPid,
@@ -38,7 +38,7 @@ import { readAppUrl } from "./app-url.js";
38
38
  import { inertInjectEnvWarnings } from "./inject-map.js";
39
39
  import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, startRedirectProxy, tearDownCa } from "./redirect-proxy.js";
40
40
  import { CaTrustError, sessionTrustEnv } from "./ca-trust.js";
41
- import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers } from "./consumers.js";
41
+ import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers, consumerProcessState } from "./consumers.js";
42
42
  import { assertLifecycleStartable, beginWorldLifecycle, handoffWorldLifecycle, recordWorldLifecycleEvent, releaseWorldLifecycle, retainWorldLifecycleForCleanup, hasWorldLifecycle, isWorldLifecycleRunning, lifecycleLogPath, releaseAbandonedWorldLifecycle, exitedWorldLifecyclePids } from "./lifecycle-record.js";
43
43
  // Births captured at actual spawn; retries never remeasure a retained PID.
44
44
  const spawnedIdentities = new Map();
@@ -510,6 +510,15 @@ function injectPreloadSpecifier() {
510
510
  // resolved on use: importing the runtime has no side effects in a host without module resolution
511
511
  return createRequire(import.meta.url).resolve('@volter/world-core/inject');
512
512
  }
513
+ /** A World's NODE_OPTIONS: its own injector, then the launcher's options without an outer World's injector. One
514
+ * injector per process (the innermost World's, which already routes on to the outer World's twins); a second one
515
+ * patches the same doors twice, and tools that re-serialize NODE_OPTIONS (Next.js's build workers) merge the two
516
+ * `--require` paths into one filename. */
517
+ function worldNodeOptions(inherited) {
518
+ const own = `--require ${injectPreloadSpecifier()}`;
519
+ const rest = (inherited ?? '').replace(/(^|\s)(?:--require|-r)[ =]\S*[\\/]world-core[\\/]inject\.cjs(?=\s|$)/g, ' ').trim();
520
+ return rest ? `${own} ${rest}` : own;
521
+ }
513
522
  function writeProxyEnv(root, name, env) {
514
523
  writeTextAtomic(proxyEnvFile(root, name), `${JSON.stringify(env, null, 2)}\n`);
515
524
  }
@@ -2162,7 +2171,7 @@ export async function upWorld(configId, options = {}, abort) {
2162
2171
  // non-loopback host once this is set. Without it "sealed" only records intent and every
2163
2172
  // untwinned host is blind-tunneled to the real internet (TWIN-60).
2164
2173
  ...(mode === 'sealed' ? { VOLTER_WORLD_SEALED: '1', VOLTER_TWIN_STRICT_EGRESS: '1' } : {}),
2165
- NODE_OPTIONS: `--require ${injectPreloadSpecifier()}${inheritedEnv.NODE_OPTIONS ? ` ${inheritedEnv.NODE_OPTIONS}` : ''}`,
2174
+ NODE_OPTIONS: worldNodeOptions(inheritedEnv.NODE_OPTIONS),
2166
2175
  };
2167
2176
  env.NO_PROXY = `127.0.0.1,localhost${env.NO_PROXY || inheritedEnv.NO_PROXY ? `,${env.NO_PROXY ?? inheritedEnv.NO_PROXY}` : ''}`;
2168
2177
  env.no_proxy = `127.0.0.1,localhost${env.no_proxy || inheritedEnv.no_proxy ? `,${env.no_proxy ?? inheritedEnv.no_proxy}` : ''}`;
@@ -3822,24 +3831,23 @@ export function retireWorldConsumers(name, root = process.cwd(), options = {}) {
3822
3831
  const chosen = consumers.filter(c => c.state === 'uncertain' && (options.consumer === undefined || c.id === options.consumer));
3823
3832
  if (options.consumer !== undefined && chosen.length === 0)
3824
3833
  throw new Error(`World "${name}" has no uncertain consumer ${options.consumer}`);
3825
- const gone = (pid) => { try {
3826
- process.kill(pid, 0);
3827
- return false;
3828
- }
3829
- catch (error) {
3830
- return error.code === 'ESRCH';
3831
- } };
3832
3834
  return chosen.map((c) => {
3833
3835
  const keep = (reason) => ({ id: c.id, owner: c.owner, retired: false, reason });
3834
3836
  if (c.hostname !== osHostname())
3835
3837
  return keep(`recorded on host ${c.hostname}; retire it there`);
3836
3838
  if (c.instanceCreatedAt !== current.createdAt)
3837
3839
  return keep('belongs to another instance of this World');
3838
- if (!gone(c.runnerPid))
3840
+ const runner = consumerProcessState(c.runnerPid);
3841
+ if (runner.state === 'unknown')
3842
+ return keep(`runner ${c.runnerPid} existence is unknown (${runner.reason})`);
3843
+ if (runner.state === 'present')
3839
3844
  return keep(`runner ${c.runnerPid} is still alive`);
3840
3845
  if (c.consumerPid === undefined)
3841
3846
  return keep('its command was never recorded as started, so its processes cannot be proven gone');
3842
- if (!gone(c.consumerPid))
3847
+ const command = consumerProcessState(c.consumerPid);
3848
+ if (command.state === 'unknown')
3849
+ return keep(`command ${c.consumerPid} existence is unknown (${command.reason})`);
3850
+ if (command.state === 'present')
3843
3851
  return keep(`command ${c.consumerPid} is still alive`);
3844
3852
  const surviving = survivingOwnedGroups([c.consumerPid]);
3845
3853
  if (surviving.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.55",
3
+ "version": "3.0.57",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -69,14 +69,14 @@
69
69
  "node": ">=22.3"
70
70
  },
71
71
  "peerDependencies": {
72
- "@volter/world-core": "3.0.55",
73
- "@volter/world-console": "3.0.50"
72
+ "@volter/world-core": "3.0.57",
73
+ "@volter/world-console": "3.0.52"
74
74
  },
75
75
  "dependencies": {
76
76
  "@electric-sql/pglite": "0.5.8",
77
77
  "@electric-sql/pglite-pgvector": "0.0.9",
78
78
  "@volter/world-access": "3.0.1",
79
- "@volter/world-core": "3.0.55",
79
+ "@volter/world-core": "3.0.57",
80
80
  "mongodb": "~6.20.0",
81
81
  "pg-gateway": "0.3.0-beta.4",
82
82
  "smol-toml": "^1.8.0",
package/src/consumers.ts CHANGED
@@ -91,10 +91,17 @@ export function releaseConsumer(root: string, name: string, record: WorldConsume
91
91
  } finally { rmSync(join(dir, `${record.id}.json`), { force: true }); }
92
92
  }
93
93
 
94
- function alive(pid: number | undefined): boolean {
95
- if (!pid) return false;
96
- try { process.kill(pid, 0); return true; }
97
- catch (error) { return (error as NodeJS.ErrnoException).code !== 'ESRCH'; }
94
+ export type ConsumerProcessState = { state: 'present' | 'absent' } | { state: 'unknown'; reason: string };
95
+
96
+ /** Signal delivery cannot prove existence. Only a successful signal-0 probe or ESRCH
97
+ * answers it; permission and registry errors retain uncertainty, never cleanup authority. */
98
+ export function consumerProcessState(pid: number): ConsumerProcessState {
99
+ try { process.kill(pid, 0); return { state: 'present' }; }
100
+ catch (error) {
101
+ const cause = error as NodeJS.ErrnoException;
102
+ if (cause?.code === 'ESRCH') return { state: 'absent' };
103
+ return { state: 'unknown', reason: cause?.code ?? cause?.message ?? String(error) };
104
+ }
98
105
  }
99
106
 
100
107
  /** Inspection never deletes records or interprets incomplete coverage as no consumers. */
@@ -126,10 +133,16 @@ export function worldConsumers(root: string, name: string, instanceCreatedAt?: s
126
133
  checkedOwner(r.owner);
127
134
  const age = Date.now() - Date.parse(r.heartbeatAt);
128
135
  const sameInstance = instanceCreatedAt === r.instanceCreatedAt;
129
- const state = r.retirementUncertain || r.hostname !== hostname() || !sameInstance ? 'uncertain'
130
- : !alive(r.runnerPid) && r.consumerPid === undefined ? 'uncertain'
131
- : !alive(r.runnerPid) && !alive(r.consumerPid) ? (r.completionRequired ? 'uncertain' : 'exited')
132
- : alive(r.runnerPid) && age >= -STALE_MS && age <= STALE_MS ? 'active' : 'uncertain';
136
+ // Never interpret another host's numeric PIDs as processes on this host.
137
+ const local = r.hostname === hostname() && sameInstance;
138
+ const runner = local ? consumerProcessState(r.runnerPid) : undefined;
139
+ const command = local && r.consumerPid !== undefined ? consumerProcessState(r.consumerPid) : undefined;
140
+ if (runner?.state === 'unknown') warnings.push(`Consumer ${r.id}: runner ${r.runnerPid} existence unknown (${runner.reason}).`);
141
+ if (command?.state === 'unknown') warnings.push(`Consumer ${r.id}: command ${r.consumerPid} existence unknown (${command.reason}).`);
142
+ const state = r.retirementUncertain || !local || runner?.state === 'unknown' || command?.state === 'unknown' ? 'uncertain'
143
+ : runner?.state === 'absent' && r.consumerPid === undefined ? 'uncertain'
144
+ : runner?.state === 'absent' && command?.state === 'absent' ? (r.completionRequired ? 'uncertain' : 'exited')
145
+ : runner?.state === 'present' && age >= -STALE_MS && age <= STALE_MS ? 'active' : 'uncertain';
133
146
  // Explicit fields only: malformed records must never leak arbitrary metadata into reports.
134
147
  consumers.push({ id: r.id, instanceCreatedAt: r.instanceCreatedAt, kind: r.kind, owner: r.owner,
135
148
  uid: r.uid, hostname: r.hostname, runnerPid: r.runnerPid, consumerPid: r.consumerPid,
package/src/runtime.ts CHANGED
@@ -41,7 +41,7 @@ import { inertInjectEnvWarnings } from './inject-map.ts';
41
41
  import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, startRedirectProxy, tearDownCa } from './redirect-proxy.ts';
42
42
  import { CaTrustError, sessionTrustEnv, type CaTrustInputs } from './ca-trust.ts';
43
43
  import type { WorldConfig, WorldExternalReadyWhen, WorldInstance, WorldIsolation, WorldMode, WorldRunOutcome, WorldRunRecord, WorldServiceConfig, WorldServiceInstance } from './schema.ts';
44
- import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers } from './consumers.ts';
44
+ import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers, consumerProcessState } from './consumers.ts';
45
45
  import { assertLifecycleStartable, beginWorldLifecycle, handoffWorldLifecycle, recordWorldLifecycleEvent, releaseWorldLifecycle, retainWorldLifecycleForCleanup, hasWorldLifecycle, isWorldLifecycleRunning, lifecycleLogPath, releaseAbandonedWorldLifecycle, exitedWorldLifecyclePids } from './lifecycle-record.ts';
46
46
 
47
47
  // Births captured at actual spawn; retries never remeasure a retained PID.
@@ -553,6 +553,16 @@ function injectPreloadSpecifier(): string {
553
553
  return createRequire(import.meta.url).resolve('@volter/world-core/inject');
554
554
  }
555
555
 
556
+ /** A World's NODE_OPTIONS: its own injector, then the launcher's options without an outer World's injector. One
557
+ * injector per process (the innermost World's, which already routes on to the outer World's twins); a second one
558
+ * patches the same doors twice, and tools that re-serialize NODE_OPTIONS (Next.js's build workers) merge the two
559
+ * `--require` paths into one filename. */
560
+ function worldNodeOptions(inherited: string | undefined): string {
561
+ const own = `--require ${injectPreloadSpecifier()}`;
562
+ const rest = (inherited ?? '').replace(/(^|\s)(?:--require|-r)[ =]\S*[\\/]world-core[\\/]inject\.cjs(?=\s|$)/g, ' ').trim();
563
+ return rest ? `${own} ${rest}` : own;
564
+ }
565
+
556
566
  function writeProxyEnv(root: string, name: string, env: Record<string, string>): void {
557
567
  writeTextAtomic(proxyEnvFile(root, name), `${JSON.stringify(env, null, 2)}\n`);
558
568
  }
@@ -2049,7 +2059,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
2049
2059
  // non-loopback host once this is set. Without it "sealed" only records intent and every
2050
2060
  // untwinned host is blind-tunneled to the real internet (TWIN-60).
2051
2061
  ...(mode === 'sealed' ? { VOLTER_WORLD_SEALED: '1', VOLTER_TWIN_STRICT_EGRESS: '1' } : {}),
2052
- NODE_OPTIONS: `--require ${injectPreloadSpecifier()}${inheritedEnv.NODE_OPTIONS ? ` ${inheritedEnv.NODE_OPTIONS}` : ''}`,
2062
+ NODE_OPTIONS: worldNodeOptions(inheritedEnv.NODE_OPTIONS),
2053
2063
  };
2054
2064
  env.NO_PROXY = `127.0.0.1,localhost${env.NO_PROXY || inheritedEnv.NO_PROXY ? `,${env.NO_PROXY ?? inheritedEnv.NO_PROXY}` : ''}`;
2055
2065
  env.no_proxy = `127.0.0.1,localhost${env.no_proxy || inheritedEnv.no_proxy ? `,${env.no_proxy ?? inheritedEnv.no_proxy}` : ''}`;
@@ -3618,14 +3628,17 @@ export function retireWorldConsumers(name: string, root = process.cwd(), options
3618
3628
  const { consumers } = worldConsumers(resolvedRoot, name, current.createdAt);
3619
3629
  const chosen = consumers.filter(c => c.state === 'uncertain' && (options.consumer === undefined || c.id === options.consumer));
3620
3630
  if (options.consumer !== undefined && chosen.length === 0) throw new Error(`World "${name}" has no uncertain consumer ${options.consumer}`);
3621
- const gone = (pid: number) => { try { process.kill(pid, 0); return false; } catch (error) { return (error as NodeJS.ErrnoException).code === 'ESRCH'; } };
3622
3631
  return chosen.map((c): ConsumerRetirement => {
3623
3632
  const keep = (reason: string): ConsumerRetirement => ({ id: c.id, owner: c.owner, retired: false, reason });
3624
3633
  if (c.hostname !== osHostname()) return keep(`recorded on host ${c.hostname}; retire it there`);
3625
3634
  if (c.instanceCreatedAt !== current.createdAt) return keep('belongs to another instance of this World');
3626
- if (!gone(c.runnerPid)) return keep(`runner ${c.runnerPid} is still alive`);
3635
+ const runner = consumerProcessState(c.runnerPid);
3636
+ if (runner.state === 'unknown') return keep(`runner ${c.runnerPid} existence is unknown (${runner.reason})`);
3637
+ if (runner.state === 'present') return keep(`runner ${c.runnerPid} is still alive`);
3627
3638
  if (c.consumerPid === undefined) return keep('its command was never recorded as started, so its processes cannot be proven gone');
3628
- if (!gone(c.consumerPid)) return keep(`command ${c.consumerPid} is still alive`);
3639
+ const command = consumerProcessState(c.consumerPid);
3640
+ if (command.state === 'unknown') return keep(`command ${c.consumerPid} existence is unknown (${command.reason})`);
3641
+ if (command.state === 'present') return keep(`command ${c.consumerPid} is still alive`);
3629
3642
  const surviving = survivingOwnedGroups([c.consumerPid]);
3630
3643
  if (surviving.length) return keep(`process group ${c.consumerPid} still has live members`);
3631
3644
  removeConsumerRecord(resolvedRoot, name, c.id);