@crouton-kit/crouter 0.3.64 → 0.3.65

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.
@@ -44,7 +44,21 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
44
44
  import { join } from 'node:path';
45
45
  import { createHarness } from '../helpers/harness.js';
46
46
  import { subscribe } from '../../canvas/canvas.js';
47
- import { isPidAlive } from '../../canvas/pid.js';
47
+ import { isPidAlive, capturePidIdentities } from '../../canvas/pid.js';
48
+ // Diagnostic-only, gated behind CRTR_DEBUG_PID_LIVENESS (same env var as the
49
+ // isRecordedPidAlive trace in pid.ts): logs whether the SIGKILLed pid reads
50
+ // alive and what identity ps reports for it, right at the two moments that
51
+ // matter for the intermittent Linux-only crash-revive wedge — directly after
52
+ // confirming the kill and again just before the grace-elapsed tick — so a real
53
+ // run shows whether the pid was reused during the grace window. No effect on
54
+ // test behavior when the env var is unset.
55
+ function debugLogPidState(label, pid) {
56
+ if (!process.env.CRTR_DEBUG_PID_LIVENESS)
57
+ return;
58
+ const identities = capturePidIdentities([pid]);
59
+ const identity = identities === null ? 'probe-null' : identities.has(pid) ? JSON.stringify(identities.get(pid)) : 'no-entry';
60
+ process.stderr.write(`[broker-crash-teardown] ${label}: pid=${pid} isPidAlive=${isPidAlive(pid)} capturePidIdentities=${identity}\n`);
61
+ }
48
62
  // crtrd.ts module const (not exported): the fresh-pi-boot grace window the daemon
49
63
  // waits before grace-reviving / boot-failing a pi observed dead. Reference:
50
64
  // crtrd.ts `REVIVE_GRACE_MS = 20_000`.
@@ -81,9 +95,11 @@ test('CRASH → grace-revive RESUME (one-writer), then clean teardown on close',
81
95
  // Actions run: crouter-development/prove-ci-fixes-in-actions). The waitFor's
82
96
  // successful resolution already IS the proof of deadness at this point.
83
97
  await h.waitFor(() => !isPidAlive(oldPid), { label: 'crashed broker pid is dead' });
98
+ debugLogPidState('after waitFor(!isPidAlive(oldPid))', oldPid);
84
99
  const NOW = 5_000_000;
85
100
  await h.tick(NOW); // pid dead, intent null → handleBrokerLiveness → handleLiveWindow marks pending
86
101
  assert.equal(h.bootCount(id), boots, 'inside the grace window → NOT yet revived');
102
+ debugLogPidState('before grace-elapsed tick', oldPid);
87
103
  await h.tick(NOW + REVIVE_GRACE_MS + 1); // grace elapsed → reviveNode(resume:true)
88
104
  // This is the file's SECOND real broker boot (crash-revive), on an
89
105
  // oversubscribed CI runner sharing the host with the other full-tier files'
@@ -74,17 +74,62 @@ export function isPidAlive(pid) {
74
74
  * discipline: a MISMATCH is the ONLY thing that reads dead, never an absence of
75
75
  * evidence. */
76
76
  export function isRecordedPidAlive(pid, expectedIdentity) {
77
- if (!isPidAlive(pid))
77
+ const debug = Boolean(process.env.CRTR_DEBUG_PID_LIVENESS);
78
+ const alive = isPidAlive(pid);
79
+ if (!alive) {
80
+ if (debug)
81
+ logPidLivenessDecision(pid, alive, expectedIdentity, 'no-probe', 'isPidAlive=false → dead', false);
78
82
  return false; // gone/zombie → dead (unchanged path)
79
- if (expectedIdentity == null)
83
+ }
84
+ if (expectedIdentity == null) {
85
+ if (debug)
86
+ logPidLivenessDecision(pid, alive, expectedIdentity, 'no-probe', 'no baseline → trust isPidAlive', true);
80
87
  return true; // no baseline → trust isPidAlive (legacy behavior)
88
+ }
81
89
  const current = capturePidIdentities([pid]);
82
- if (current == null)
90
+ if (current == null) {
91
+ if (debug)
92
+ logPidLivenessDecision(pid, alive, expectedIdentity, 'probe-null', 'ps probe failed → fail-open alive', true);
83
93
  return true; // probe itself failed → fail-open
94
+ }
84
95
  const actual = current.get(pid);
85
- if (actual === undefined)
96
+ if (actual === undefined) {
97
+ if (debug)
98
+ logPidLivenessDecision(pid, alive, expectedIdentity, 'no-entry', 'no identity row for pid (race) → fail-open alive', true);
86
99
  return true; // present but no identity row (race) → fail-open
87
- return actual === expectedIdentity; // different identity → REUSED → dead
100
+ }
101
+ const result = actual === expectedIdentity; // different identity → REUSED → dead
102
+ if (debug)
103
+ logPidLivenessDecision(pid, alive, expectedIdentity, 'value', result ? 'identity matches expected → alive' : 'identity differs from expected → REUSED → dead', result, actual);
104
+ return result;
105
+ }
106
+ /** Diagnostic-only decision trace for `isRecordedPidAlive`, gated entirely
107
+ * behind `CRTR_DEBUG_PID_LIVENESS` — callers must check the env var
108
+ * themselves before calling this so the (tiny) formatting cost is paid ONLY
109
+ * when the env var is set; this function does no env check of its own.
110
+ * Written to trace the intermittent Linux-only CI crash-revive wedge
111
+ * (`broker-crash-teardown.test.ts`'s 'CRASH → grace-revive RESUME'): the
112
+ * goal is for a single stderr line per call to make BOTH a passing run ("pid
113
+ * read dead → revive") and a wedged run ("read alive via <branch>") fully
114
+ * self-explaining, without changing any return value. `capturePidIdentities`
115
+ * outcome is reported as one of three DISTINCT cases — `probe-null` (the
116
+ * `ps` probe itself failed), `no-entry` (probe succeeded but the map has no
117
+ * row for this pid), or `value` (probe succeeded and found an identity,
118
+ * printed verbatim) — plus `no-probe` for the two branches that return
119
+ * before `capturePidIdentities` is ever called. Kept as permanent
120
+ * observability for this primitive, not a throwaway debug print: pid-reuse
121
+ * liveness is subtle enough that the next person chasing a wedge here should
122
+ * find this already in place. */
123
+ function logPidLivenessDecision(pid, isPidAliveResult, expectedIdentity, probeOutcome, branch, returned, actualIdentity) {
124
+ const parts = [
125
+ `pid=${String(pid)}`,
126
+ `isPidAlive=${isPidAliveResult}`,
127
+ `expectedIdentity=${expectedIdentity === undefined ? 'undefined' : expectedIdentity === null ? 'null' : JSON.stringify(expectedIdentity)}`,
128
+ `capturePidIdentities=${probeOutcome}${probeOutcome === 'value' ? `(${JSON.stringify(actualIdentity)})` : ''}`,
129
+ `branch=${JSON.stringify(branch)}`,
130
+ `returned=${returned}`,
131
+ ];
132
+ process.stderr.write(`[isRecordedPidAlive] ${parts.join(' ')}\n`);
88
133
  }
89
134
  /** SIGTERM a process GROUP by pid (the negative-pid convention) — best-effort,
90
135
  * swallowing ESRCH (already gone). Shared by triggers.ts (cancel-time reap of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.64",
3
+ "version": "0.3.65",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",