@houwert/conductor 0.8.0 → 0.9.0

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.
@@ -41,6 +41,26 @@ const cdpUrl = process.env.CONDUCTOR_CDP_URL || undefined;
41
41
  * URL heuristics.
42
42
  */
43
43
  const cdpTargetId = process.env.CONDUCTOR_CDP_TARGET_ID || undefined;
44
+ /**
45
+ * PID of the process that should be considered the daemon's "owner". When set,
46
+ * the daemon polls for this process's existence and shuts down cleanly when it
47
+ * disappears. This prevents orphaned daemons (and their Playwright browsers)
48
+ * from piling up after the host app crashes or quits without calling
49
+ * `daemon-stop`.
50
+ *
51
+ * The daemon runs detached, so `process.ppid` becomes 1 after the parent exits
52
+ * and is useless for this purpose. The owner must be passed explicitly by the
53
+ * host app via the env when it invokes `conductor daemon-start` (or whatever
54
+ * code path ultimately triggers the daemon spawn).
55
+ */
56
+ const parentPid = (() => {
57
+ const raw = process.env.CONDUCTOR_PARENT_PID;
58
+ if (!raw)
59
+ return undefined;
60
+ const n = parseInt(raw, 10);
61
+ return Number.isFinite(n) && n > 0 ? n : undefined;
62
+ })();
63
+ const PARENT_POLL_INTERVAL_MS = 10000;
44
64
  const SOCKET_PATH = (0, protocol_js_1.socketPath)(sessionName);
45
65
  const PID_FILE = (0, protocol_js_1.pidFile)(sessionName);
46
66
  const LOG_FILE = (0, protocol_js_1.logFile)(sessionName);
@@ -123,6 +143,7 @@ async function main() {
123
143
  }
124
144
  let idleTimer;
125
145
  let healthTimer;
146
+ let parentWatchTimer;
126
147
  const idleTimeoutMs = Number(process.env.CONDUCTOR_IDLE_TIMEOUT_MS) || protocol_js_1.IDLE_TIMEOUT_MS;
127
148
  function resetIdleTimer() {
128
149
  if (idleTimer)
@@ -135,6 +156,10 @@ async function main() {
135
156
  async function cleanup() {
136
157
  if (healthTimer)
137
158
  clearInterval(healthTimer);
159
+ if (parentWatchTimer)
160
+ clearInterval(parentWatchTimer);
161
+ if (idleTimer)
162
+ clearTimeout(idleTimer);
138
163
  if (logCollector) {
139
164
  logCollector.stop();
140
165
  logCollector = null;
@@ -215,6 +240,30 @@ async function main() {
215
240
  }, DRIVER_HEALTH_INTERVAL_MS);
216
241
  healthTimer.unref(); // Don't keep the process alive just for health checks
217
242
  }
243
+ // If the host app told us who it is, shut down when it disappears. This is
244
+ // the primary defence against orphaned daemons + headless Chromiums when the
245
+ // host app crashes or force-quits without calling daemon-stop.
246
+ if (parentPid !== undefined) {
247
+ dlog(`Watching parent pid ${parentPid}`);
248
+ let shuttingDown = false;
249
+ parentWatchTimer = setInterval(() => {
250
+ if (shuttingDown)
251
+ return;
252
+ try {
253
+ process.kill(parentPid, 0);
254
+ }
255
+ catch (err) {
256
+ const code = err.code;
257
+ if (code === 'ESRCH') {
258
+ shuttingDown = true;
259
+ dlog(`Parent pid ${parentPid} exited — shutting down`);
260
+ cleanup().then(() => process.exit(0));
261
+ }
262
+ // EPERM means the process exists but we can't signal it — still alive.
263
+ }
264
+ }, PARENT_POLL_INTERVAL_MS);
265
+ parentWatchTimer.unref();
266
+ }
218
267
  // ── HTTP server on Unix socket ─────────────────────────────────────────────
219
268
  // Replaces the old raw-TCP accept-and-close with a proper HTTP server so we
220
269
  // can serve /status (aliveness) and /logs (buffered log entries).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor
3
- version: 0.8.0
3
+ version: 0.9.0
4
4
  description: "Token-efficient CLI for mobile UI testing (iOS simulator + Android emulator), designed for AI agents"
5
5
  metadata.openclaw:
6
6
  category: service
@@ -3,6 +3,6 @@ skills:
3
3
  path: conductor/SKILL.md
4
4
  description: "Token-efficient CLI for mobile UI testing, designed for AI agents"
5
5
  category: service
6
- version: 0.8.0
6
+ version: 0.9.0
7
7
  requires:
8
8
  bins: [conductor]