@thezzisu/droidnode 0.140.1 → 0.142.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.
Files changed (2) hide show
  1. package/bin/droidnode.js +79 -13
  2. package/package.json +2 -2
package/bin/droidnode.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // © Factory AI for droid itself, its JS bundle, and `@factory/cli-*` binaries
12
12
  // — see README.md. This launcher is MIT.
13
13
 
14
- import { spawnSync } from 'node:child_process';
14
+ import { spawn, spawnSync } from 'node:child_process';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { dirname, join, resolve } from 'node:path';
17
17
  import { findDroidBinary } from '../lib/locate.js';
@@ -70,17 +70,83 @@ const env = {
70
70
  DROIDNODE_DROID_BIN: droid.path,
71
71
  };
72
72
 
73
- // `--import` (Node 19+, stable 20.6+) loads bun-shim.mjs before bundle.
74
- // `--enable-source-maps` keeps stack traces readable across our patches.
75
- const result = spawnSync(
76
- process.execPath,
77
- ['--import', PRELOAD_URL, '--enable-source-maps', droidMjs, ...args],
78
- { stdio: 'inherit', env },
79
- );
80
-
81
- if (result.error) die(`failed to spawn node: ${result.error.message}`);
82
- if (result.signal) {
83
- process.kill(process.pid, result.signal);
73
+ // ─── IPC channel pass-through ───
74
+ // droid's in-process daemon runtime spawns one `droid exec --input-format
75
+ // stream-jsonrpc` backend per session (including every mission worker) with a
76
+ // 4th `ipc` stdio fd and `serialization:"json"`. The backend detects that
77
+ // channel via `typeof process.send === "function"` and, when present, streams
78
+ // its live activity notifications (create_message / tool_call /
79
+ // tool_progress_update / droid_working_state_changed …) back to the organizer's
80
+ // session store over IPC — that store is exactly what MissionControl renders.
81
+ //
82
+ // Because our shim rewrites process.execPath to this wrapper, droid re-spawns
83
+ // `node droidnode exec …` instead of the bundle directly, inserting this
84
+ // process between the runtime and the real bundle. A plain spawnSync with
85
+ // stdio:'inherit' forwards only fd 0/1/2, so Node strips NODE_CHANNEL_FD from
86
+ // the bundle child: the backend falls back to an isolated "parent" runtime,
87
+ // the RPC/stdout path still works (mission completes, next worker spawns), but
88
+ // worker activity never reaches the organizer → missionControl.noWorkerActivity.
89
+ //
90
+ // Fix: when WE were spawned with an inherited IPC channel, give the bundle child
91
+ // its own `ipc` fd and transparently relay JSON messages both ways. Outside that
92
+ // path (user runs `droidnode` in a terminal) there's no parent channel and we
93
+ // keep the original stdio:'inherit' behavior unchanged.
94
+ const parentHasIpc = typeof process.send === 'function' && process.channel != null;
95
+
96
+ if (parentHasIpc) {
97
+ // Let Node mint a fresh IPC channel for the child; drop the inherited
98
+ // descriptor so the child doesn't try to reuse our parent's fd.
99
+ const childEnv = { ...env };
100
+ delete childEnv.NODE_CHANNEL_FD;
101
+ delete childEnv.NODE_CHANNEL_SERIALIZATION_MODE;
102
+
103
+ const child = spawn(
104
+ process.execPath,
105
+ ['--import', PRELOAD_URL, '--enable-source-maps', droidMjs, ...args],
106
+ { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], serialization: 'json', env: childEnv },
107
+ );
108
+
109
+ // Transparent relay: runtime (our parent) ⇄ this wrapper ⇄ bundle (child).
110
+ process.on('message', (m) => { try { child.send(m); } catch { /* child gone */ } });
111
+ child.on('message', (m) => { try { process.send(m); } catch { /* parent gone */ } });
112
+ process.on('disconnect', () => { try { child.disconnect(); } catch { /* already closed */ } });
113
+ child.on('disconnect', () => { try { process.disconnect(); } catch { /* already closed */ } });
114
+
115
+ // Forward terminating signals so Ctrl-C / daemon shutdown reaches the bundle.
116
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) {
117
+ process.on(sig, () => { try { child.kill(sig); } catch { /* already exited */ } });
118
+ }
119
+
120
+ let spawned = false;
121
+ let exited = false;
122
+ const finish = (code, signal) => {
123
+ if (exited) return;
124
+ exited = true;
125
+ if (signal) process.kill(process.pid, signal);
126
+ else process.exit(code ?? 0);
127
+ };
128
+ child.once('spawn', () => { spawned = true; });
129
+ child.on('error', (e) => {
130
+ // Before spawn: a real launch failure (ENOENT, EACCES…) → fatal.
131
+ // After spawn: IPC-channel teardown races when the child exits without
132
+ // using the channel (e.g. `--version`) surface here as benign errors;
133
+ // ignore them and let the `exit` event carry the real status.
134
+ if (!spawned) die(`failed to spawn node: ${e.message}`);
135
+ });
136
+ child.on('exit', (code, signal) => finish(code, signal));
84
137
  } else {
85
- process.exit(result.status ?? 0);
138
+ // `--import` (Node 19+, stable 20.6+) loads bun-shim.mjs before bundle.
139
+ // `--enable-source-maps` keeps stack traces readable across our patches.
140
+ const result = spawnSync(
141
+ process.execPath,
142
+ ['--import', PRELOAD_URL, '--enable-source-maps', droidMjs, ...args],
143
+ { stdio: 'inherit', env },
144
+ );
145
+
146
+ if (result.error) die(`failed to spawn node: ${result.error.message}`);
147
+ if (result.signal) {
148
+ process.kill(process.pid, result.signal);
149
+ } else {
150
+ process.exit(result.status ?? 0);
151
+ }
86
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thezzisu/droidnode",
3
- "version": "0.140.1",
3
+ "version": "0.142.0",
4
4
  "description": "Run Factory's droid CLI on plain Node.js. Sidesteps the Bun 1.3.x standalone-init NULL-allocator race that crashes `droid --resume`, and Bun's long-session memory leaks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "dependencies": {
17
- "droid": "^0.140.0",
17
+ "droid": "^0.142.0",
18
18
  "koffi": "^2.10.0",
19
19
  "ws": "^8.18.0"
20
20
  },