@north-light/crouter 0.3.167 → 0.3.168

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.
@@ -34,7 +34,6 @@ import { tmpdir } from 'node:os';
34
34
  import { dirname, join } from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
36
  import { createAgentSessionServices, createAgentSessionFromServices, SessionManager, VERSION, } from '../runtime/broker-sdk.js';
37
- import { CANVAS_EXTENSIONS } from '../runtime/launch.js';
38
37
  import { buildBrokerSession } from '../runtime/broker.js';
39
38
  // M-9 (broker.ts fault-and-die model gate): buildBrokerSession with no cfg.model
40
39
  // now calls the REAL SDK's modelRegistry.getAvailable() and faults if it's empty
@@ -83,7 +82,7 @@ test('D2 broker fork branch: buildBrokerSession(cfg.forkFrom) yields a NEW id +
83
82
  // if broker.ts:~1106 reverts to that throw, this rejects and the test goes RED.
84
83
  const cfg = {
85
84
  cwd: dir,
86
- extensionPaths: [...CANVAS_EXTENSIONS, C3_EXT],
85
+ extensionPaths: [C3_EXT],
87
86
  forkFrom: srcFile,
88
87
  model: 'c3prov/c3model',
89
88
  };
@@ -84,7 +84,7 @@ function cfg(cwd, extra = {}) {
84
84
  test('C3 — services path registers an extension model provider; the broker session gets it', async () => {
85
85
  const cwd = mkdtempSync(join(tmpdir(), 'crtr-c3-'));
86
86
  try {
87
- const { session, services } = await buildBrokerSession(realEngine, cfg(cwd, { model: 'c3prov/c3model' }));
87
+ const { session, services } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C3_EXT], model: 'c3prov/c3model' }));
88
88
  try {
89
89
  // The extension's provider was registered into the SERVICES model runtime (this
90
90
  // is the registerProvider step plain createAgentSession skips).
@@ -107,7 +107,7 @@ test('C3 — services path registers an extension model provider; the broker ses
107
107
  test('C3b — broker splits model thinking suffix before SDK registry lookup', async () => {
108
108
  const cwd = mkdtempSync(join(tmpdir(), 'crtr-c3b-'));
109
109
  try {
110
- const { session } = await buildBrokerSession(realEngine, cfg(cwd, { model: 'c3prov/c3model:high' }));
110
+ const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C3_EXT], model: 'c3prov/c3model:high' }));
111
111
  try {
112
112
  assert.equal(session.model?.id, 'c3model', 'C3b: suffixed model specs resolve by model id instead of falling back to SDK default');
113
113
  }
@@ -420,7 +420,7 @@ test('C5 — isLeadingEngineCommand against a REAL session (registered extension
420
420
  // proven against a plain object shaped like the real accessors.
421
421
  const cwd = mkdtempSync(join(tmpdir(), 'crtr-c5-cmd-'));
422
422
  try {
423
- const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [...brokerExts(), C5_EXT], model: 'c3prov/c3model' }));
423
+ const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C3_EXT, C5_EXT], model: 'c3prov/c3model' }));
424
424
  try {
425
425
  assert.equal(isLeadingEngineCommand('/devcmd', session), true, 'a real registered extension command matches');
426
426
  assert.equal(isLeadingEngineCommand('/devcmd extra args', session), true, 'space-separated still matches against a real session');
@@ -744,7 +744,7 @@ test('Major 6 (broker half) — engineLeadingCommandTokens degrades to empty per
744
744
  test('Major 6 (broker half) — engineLeadingCommandTokens against a REAL session enumerates the real registered extension command', async () => {
745
745
  const cwd = mkdtempSync(join(tmpdir(), 'crtr-m6-cmd-'));
746
746
  try {
747
- const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [...brokerExts(), C5_EXT], model: 'c3prov/c3model' }));
747
+ const { session } = await buildBrokerSession(realEngine, cfg(cwd, { extensionPaths: [C3_EXT, C5_EXT], model: 'c3prov/c3model' }));
748
748
  try {
749
749
  const tokens = engineLeadingCommandTokens(session);
750
750
  assert.ok(tokens.includes('devcmd'), 'the real registered extension command name is present in the enumeration');
@@ -1,17 +1,8 @@
1
- /** True if a process with `pid` is currently alive (signal-0 probe) AND not a
2
- * zombie. `kill(pid, 0)` throws ESRCH when the process is gone; EPERM means
3
- * it exists but isn't ours — still alive (a foreign process is never OUR
4
- * zombie, so the `isZombie` check is skipped for that branch — see below). A
5
- * null/undefined pid (legacy / never-booted) reads dead.
6
- *
7
- * The zombie check matters because EVERY broker this module supervises is
8
- * spawned `detached: true` by crtr itself (`headlessBrokerHost.launch`) — a
9
- * dead broker whose spawning process hasn't reaped it yet is a zombie, and
10
- * `kill(pid, 0)` alone reports it as alive, wedging the daemon's revive guard
11
- * (`isPidAlive(pi_pid)`, `revive.ts`) forever: the daemon never revives an
12
- * already-live-looking row, so a refreshed/crashed node whose broker zombied
13
- * out under a long-lived spawning process (see `isZombie`'s doc) never comes
14
- * back on its own. */
1
+ /** True if a process with `pid` is currently alive. `kill(pid, 0)` provides the
2
+ * initial existence check; EPERM means the foreign process exists. For our
3
+ * processes, the later `ps` read is authoritative because it also distinguishes
4
+ * zombies and processes reaped between the two probes. Only an unavailable
5
+ * `ps` probe fails open to alive. A null/undefined pid reads dead. */
15
6
  export declare function isPidAlive(pid: number | null | undefined): boolean;
16
7
  /** THE single precision-aware identity compare, used EVERYWHERE two
17
8
  * `composeIdentity` fingerprints are compared (this module's own
@@ -10,46 +10,31 @@
10
10
  import { spawnSync } from 'node:child_process';
11
11
  import { readFileSync } from 'node:fs';
12
12
  import { readKernelBootId } from './boot-id.js';
13
- /** Is `pid` a ZOMBIE (exited, awaiting reap by its parent)? `kill(pid, 0)`
14
- * reports a zombie as a perfectly normal alive process — its PID is still
15
- * occupied in the process table — so callers relying on that signal-0 probe
16
- * alone (see `isPidAlive`) misread a dead broker as alive forever whenever
17
- * its spawning process never reaps it (crouton-labs zombie-broker bug: the
18
- * front door's foreground attach session parks its OWN event loop for the
19
- * whole session — see `bootRoot` in runtime/boot-root.ts — so it can't process
20
- * the SIGCHLD that would otherwise reap its detached broker child in real
21
- * time). `ps -o stat=` reports a leading `Z` for a zombie on BOTH BSD ps
22
- * (macOS) and procps-ng (Linux), so one portable probe covers both platforms
23
- * without a Linux-only `/proc` special case. Best-effort: any probe failure
24
- * (spawn error, no matching row, non-zero exit) reads as NOT a zombie —
25
- * fail-open, matching this module's other guards; a probe failure must never
26
- * be misread as proof of death (a live pid IS alive; only a positive `Z`
27
- * read says otherwise). */
28
- function isZombie(pid) {
13
+ /** Classify the current `ps` row for a pid. BSD ps and procps-ng both report a
14
+ * leading `Z` for zombies and exit silently with empty output when no process
15
+ * matches. Only a probe that could not run is unknown; that is the sole
16
+ * fail-open result. */
17
+ function psProcessState(pid) {
29
18
  try {
30
19
  const r = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], { encoding: 'utf8', timeout: 2000 });
31
- if (r.status !== 0 || typeof r.stdout !== 'string')
32
- return false;
33
- return r.stdout.trim().charAt(0) === 'Z';
20
+ if (r.error != null || r.signal != null || typeof r.stdout !== 'string')
21
+ return 'unknown';
22
+ if (typeof r.stderr === 'string' && r.stderr.trim() !== '')
23
+ return 'unknown';
24
+ const stat = r.stdout.trim();
25
+ if (stat === '')
26
+ return 'gone';
27
+ return stat.charAt(0) === 'Z' ? 'zombie' : 'live';
34
28
  }
35
29
  catch {
36
- return false;
30
+ return 'unknown';
37
31
  }
38
32
  }
39
- /** True if a process with `pid` is currently alive (signal-0 probe) AND not a
40
- * zombie. `kill(pid, 0)` throws ESRCH when the process is gone; EPERM means
41
- * it exists but isn't ours — still alive (a foreign process is never OUR
42
- * zombie, so the `isZombie` check is skipped for that branch — see below). A
43
- * null/undefined pid (legacy / never-booted) reads dead.
44
- *
45
- * The zombie check matters because EVERY broker this module supervises is
46
- * spawned `detached: true` by crtr itself (`headlessBrokerHost.launch`) — a
47
- * dead broker whose spawning process hasn't reaped it yet is a zombie, and
48
- * `kill(pid, 0)` alone reports it as alive, wedging the daemon's revive guard
49
- * (`isPidAlive(pi_pid)`, `revive.ts`) forever: the daemon never revives an
50
- * already-live-looking row, so a refreshed/crashed node whose broker zombied
51
- * out under a long-lived spawning process (see `isZombie`'s doc) never comes
52
- * back on its own. */
33
+ /** True if a process with `pid` is currently alive. `kill(pid, 0)` provides the
34
+ * initial existence check; EPERM means the foreign process exists. For our
35
+ * processes, the later `ps` read is authoritative because it also distinguishes
36
+ * zombies and processes reaped between the two probes. Only an unavailable
37
+ * `ps` probe fails open to alive. A null/undefined pid reads dead. */
53
38
  export function isPidAlive(pid) {
54
39
  if (pid == null)
55
40
  return false;
@@ -59,7 +44,8 @@ export function isPidAlive(pid) {
59
44
  catch (e) {
60
45
  return e.code === 'EPERM';
61
46
  }
62
- return !isZombie(pid);
47
+ const state = psProcessState(pid);
48
+ return state !== 'zombie' && state !== 'gone';
63
49
  }
64
50
  /** Matches the kernel `boot_id` UUID shape (`/proc/sys/kernel/random/boot_id`),
65
51
  * used to tell a NEW-format identity base (a per-boot UUID) apart from a