@volter/world-runtime 3.0.56 → 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();
@@ -3831,24 +3831,23 @@ export function retireWorldConsumers(name, root = process.cwd(), options = {}) {
3831
3831
  const chosen = consumers.filter(c => c.state === 'uncertain' && (options.consumer === undefined || c.id === options.consumer));
3832
3832
  if (options.consumer !== undefined && chosen.length === 0)
3833
3833
  throw new Error(`World "${name}" has no uncertain consumer ${options.consumer}`);
3834
- const gone = (pid) => { try {
3835
- process.kill(pid, 0);
3836
- return false;
3837
- }
3838
- catch (error) {
3839
- return error.code === 'ESRCH';
3840
- } };
3841
3834
  return chosen.map((c) => {
3842
3835
  const keep = (reason) => ({ id: c.id, owner: c.owner, retired: false, reason });
3843
3836
  if (c.hostname !== osHostname())
3844
3837
  return keep(`recorded on host ${c.hostname}; retire it there`);
3845
3838
  if (c.instanceCreatedAt !== current.createdAt)
3846
3839
  return keep('belongs to another instance of this World');
3847
- 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')
3848
3844
  return keep(`runner ${c.runnerPid} is still alive`);
3849
3845
  if (c.consumerPid === undefined)
3850
3846
  return keep('its command was never recorded as started, so its processes cannot be proven gone');
3851
- 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')
3852
3851
  return keep(`command ${c.consumerPid} is still alive`);
3853
3852
  const surviving = survivingOwnedGroups([c.consumerPid]);
3854
3853
  if (surviving.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.56",
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.56",
73
- "@volter/world-console": "3.0.51"
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.56",
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.
@@ -3628,14 +3628,17 @@ export function retireWorldConsumers(name: string, root = process.cwd(), options
3628
3628
  const { consumers } = worldConsumers(resolvedRoot, name, current.createdAt);
3629
3629
  const chosen = consumers.filter(c => c.state === 'uncertain' && (options.consumer === undefined || c.id === options.consumer));
3630
3630
  if (options.consumer !== undefined && chosen.length === 0) throw new Error(`World "${name}" has no uncertain consumer ${options.consumer}`);
3631
- const gone = (pid: number) => { try { process.kill(pid, 0); return false; } catch (error) { return (error as NodeJS.ErrnoException).code === 'ESRCH'; } };
3632
3631
  return chosen.map((c): ConsumerRetirement => {
3633
3632
  const keep = (reason: string): ConsumerRetirement => ({ id: c.id, owner: c.owner, retired: false, reason });
3634
3633
  if (c.hostname !== osHostname()) return keep(`recorded on host ${c.hostname}; retire it there`);
3635
3634
  if (c.instanceCreatedAt !== current.createdAt) return keep('belongs to another instance of this World');
3636
- 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`);
3637
3638
  if (c.consumerPid === undefined) return keep('its command was never recorded as started, so its processes cannot be proven gone');
3638
- 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`);
3639
3642
  const surviving = survivingOwnedGroups([c.consumerPid]);
3640
3643
  if (surviving.length) return keep(`process group ${c.consumerPid} still has live members`);
3641
3644
  removeConsumerRecord(resolvedRoot, name, c.id);