@addai/node 0.13.0 → 0.15.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.
package/dist/command-runner.js
CHANGED
|
@@ -621,6 +621,9 @@ async function runDesktopCommand(cmd) {
|
|
|
621
621
|
await (0, manager_1.setStatus)(row.id, {
|
|
622
622
|
status: 'running', container_id: containerId,
|
|
623
623
|
engine: provider.id, vnc_port: vncPort, status_message: null,
|
|
624
|
+
// Record the password we actually gave the container, or the viewer
|
|
625
|
+
// will keep presenting the previous one.
|
|
626
|
+
vnc_password: vncPassword,
|
|
624
627
|
});
|
|
625
628
|
await (0, creds_1.syncCredentials)(provider, built, onLog);
|
|
626
629
|
if (row.setup_script) {
|
package/dist/desktop/docker.js
CHANGED
|
@@ -42,8 +42,16 @@ class CliProvider {
|
|
|
42
42
|
// payload as its own phase rather than hiding inside `run`.
|
|
43
43
|
onLog(`pulling ${row.image}\n`);
|
|
44
44
|
const pull = await this.run(['pull', row.image], onLog);
|
|
45
|
-
if (!pull.ok)
|
|
46
|
-
|
|
45
|
+
if (!pull.ok) {
|
|
46
|
+
// A pull failure is only fatal if we have no copy. A locally built or
|
|
47
|
+
// side-loaded image has no registry to pull FROM, and a node that is
|
|
48
|
+
// briefly offline should still be able to start a desktop it already
|
|
49
|
+
// has. Only give up when neither the registry nor the disk has it.
|
|
50
|
+
const local = await this.run(['image', 'inspect', row.image], undefined, 30_000);
|
|
51
|
+
if (!local.ok)
|
|
52
|
+
throw new Error(`image pull failed: ${pull.stderr.trim().slice(0, 500)}`);
|
|
53
|
+
onLog(`pull failed; using the copy already on this machine\n`);
|
|
54
|
+
}
|
|
47
55
|
onLog(`creating ${(0, provider_1.containerName)(row)}\n`);
|
|
48
56
|
const res = await this.run((0, provider_1.buildCreateArgs)(row, opts), onLog);
|
|
49
57
|
if (!res.ok)
|
|
@@ -12,6 +12,10 @@ export declare function setStatus(id: string, fields: {
|
|
|
12
12
|
container_id?: string | null;
|
|
13
13
|
engine?: string | null;
|
|
14
14
|
vnc_port?: number | null;
|
|
15
|
+
/** Written only on create/rebuild, when the daemon mints a fresh one. The
|
|
16
|
+
* viewer authenticates with whatever is recorded here, so a password used
|
|
17
|
+
* but not recorded means "password check failed" at the RFB handshake. */
|
|
18
|
+
vnc_password?: string | null;
|
|
15
19
|
}): Promise<void>;
|
|
16
20
|
/** A free loopback port for this desktop's VNC. Asking the OS for port 0 and
|
|
17
21
|
* reading back what it bound is the only race-free way to pick one. */
|
package/dist/desktop/manager.js
CHANGED
|
@@ -50,16 +50,28 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
50
50
|
const net = __importStar(require("net"));
|
|
51
51
|
const store_1 = require("../store");
|
|
52
52
|
const manager_1 = require("./manager");
|
|
53
|
-
|
|
53
|
+
// The deployed relay. Overridable so a node can be pointed at a local one.
|
|
54
|
+
const RELAY_URL = process.env.AINODE_RELAY_URL
|
|
55
|
+
?? 'wss://desktop-relay-29522465016.europe-west2.run.app';
|
|
54
56
|
const RECONNECT_MIN_MS = 2_000;
|
|
55
57
|
const RECONNECT_MAX_MS = 60_000;
|
|
56
58
|
/** Re-check whether any desktop is running this often while idle. */
|
|
57
59
|
const IDLE_CHECK_MS = 60_000;
|
|
60
|
+
/** How often to prove the socket is still alive. A machine that suspends
|
|
61
|
+
* leaves a half-open socket that never fires 'close', so without this the
|
|
62
|
+
* client sits holding a dead handle and never reconnects. */
|
|
63
|
+
const PING_INTERVAL_MS = 30_000;
|
|
58
64
|
let ws = null;
|
|
59
65
|
let stopped = true;
|
|
60
66
|
let backoff = RECONNECT_MIN_MS;
|
|
61
67
|
let idleTimer = null;
|
|
62
|
-
|
|
68
|
+
let liveTimer = null;
|
|
69
|
+
/** Set on every pong; cleared when a ping goes out. Two misses = dead. */
|
|
70
|
+
let missedPongs = 0;
|
|
71
|
+
/** viewerId -> that viewer's OWN TCP socket to the desktop's RFB port.
|
|
72
|
+
* Keyed per viewer, not per desktop: RFB is a stateful 1:1 protocol, so two
|
|
73
|
+
* people watching the same screen each need their own connection and their
|
|
74
|
+
* own handshake. x11vnc runs with -shared precisely so it will serve them. */
|
|
63
75
|
const bridges = new Map();
|
|
64
76
|
function dropBridges() {
|
|
65
77
|
for (const s of bridges.values()) {
|
|
@@ -99,7 +111,35 @@ async function connect() {
|
|
|
99
111
|
}
|
|
100
112
|
const sock = new ws_1.default(`${RELAY_URL}/node?token=${encodeURIComponent(token)}`);
|
|
101
113
|
ws = sock;
|
|
102
|
-
sock.on('open', () => {
|
|
114
|
+
sock.on('open', () => {
|
|
115
|
+
backoff = RECONNECT_MIN_MS;
|
|
116
|
+
missedPongs = 0;
|
|
117
|
+
if (liveTimer)
|
|
118
|
+
clearInterval(liveTimer);
|
|
119
|
+
// A suspended Mac comes back with a socket that looks open and is not.
|
|
120
|
+
// Ping until two go unanswered, then tear it down so the normal backoff
|
|
121
|
+
// path reconnects - the daemon's own sleep detector reports the same
|
|
122
|
+
// suspends this is guarding against.
|
|
123
|
+
liveTimer = setInterval(() => {
|
|
124
|
+
if (sock.readyState !== ws_1.default.OPEN) {
|
|
125
|
+
sock.terminate();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (missedPongs >= 2) {
|
|
129
|
+
console.warn('[relay] no pong in 60s — assuming the link died, reconnecting');
|
|
130
|
+
sock.terminate();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
missedPongs += 1;
|
|
134
|
+
try {
|
|
135
|
+
sock.ping();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
sock.terminate();
|
|
139
|
+
}
|
|
140
|
+
}, PING_INTERVAL_MS);
|
|
141
|
+
});
|
|
142
|
+
sock.on('pong', () => { missedPongs = 0; });
|
|
103
143
|
sock.on('message', async (raw) => {
|
|
104
144
|
let msg;
|
|
105
145
|
try {
|
|
@@ -118,27 +158,31 @@ async function connect() {
|
|
|
118
158
|
tcp.on('data', chunk => {
|
|
119
159
|
if (sock.readyState === ws_1.default.OPEN) {
|
|
120
160
|
sock.send(JSON.stringify({
|
|
121
|
-
type: 'data',
|
|
161
|
+
type: 'data', viewerId: msg.viewerId, b64: chunk.toString('base64'),
|
|
122
162
|
}));
|
|
123
163
|
}
|
|
124
164
|
});
|
|
125
|
-
const forget = () => { bridges.delete(msg.
|
|
165
|
+
const forget = () => { bridges.delete(msg.viewerId); };
|
|
126
166
|
tcp.on('close', forget);
|
|
127
167
|
tcp.on('error', forget);
|
|
128
|
-
bridges.get(msg.
|
|
129
|
-
bridges.set(msg.
|
|
168
|
+
bridges.get(msg.viewerId)?.destroy();
|
|
169
|
+
bridges.set(msg.viewerId, tcp);
|
|
130
170
|
return;
|
|
131
171
|
}
|
|
132
172
|
if (msg.type === 'data' && msg.b64) {
|
|
133
|
-
bridges.get(msg.
|
|
173
|
+
bridges.get(msg.viewerId)?.write(Buffer.from(msg.b64, 'base64'));
|
|
134
174
|
return;
|
|
135
175
|
}
|
|
136
176
|
if (msg.type === 'detach') {
|
|
137
|
-
bridges.get(msg.
|
|
138
|
-
bridges.delete(msg.
|
|
177
|
+
bridges.get(msg.viewerId)?.destroy();
|
|
178
|
+
bridges.delete(msg.viewerId);
|
|
139
179
|
}
|
|
140
180
|
});
|
|
141
181
|
const retry = () => {
|
|
182
|
+
if (liveTimer) {
|
|
183
|
+
clearInterval(liveTimer);
|
|
184
|
+
liveTimer = null;
|
|
185
|
+
}
|
|
142
186
|
if (ws !== sock)
|
|
143
187
|
return; // superseded by a newer socket
|
|
144
188
|
dropBridges();
|
|
@@ -161,6 +205,10 @@ function stopRelayClient() {
|
|
|
161
205
|
clearTimeout(idleTimer);
|
|
162
206
|
idleTimer = null;
|
|
163
207
|
}
|
|
208
|
+
if (liveTimer) {
|
|
209
|
+
clearInterval(liveTimer);
|
|
210
|
+
liveTimer = null;
|
|
211
|
+
}
|
|
164
212
|
dropBridges();
|
|
165
213
|
try {
|
|
166
214
|
ws?.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|