@addai/node 0.13.0 → 0.14.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,15 +50,24 @@ 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;
|
|
68
|
+
let liveTimer = null;
|
|
69
|
+
/** Set on every pong; cleared when a ping goes out. Two misses = dead. */
|
|
70
|
+
let missedPongs = 0;
|
|
62
71
|
/** desktopId -> the local TCP socket to that desktop's published RFB port. */
|
|
63
72
|
const bridges = new Map();
|
|
64
73
|
function dropBridges() {
|
|
@@ -99,7 +108,35 @@ async function connect() {
|
|
|
99
108
|
}
|
|
100
109
|
const sock = new ws_1.default(`${RELAY_URL}/node?token=${encodeURIComponent(token)}`);
|
|
101
110
|
ws = sock;
|
|
102
|
-
sock.on('open', () => {
|
|
111
|
+
sock.on('open', () => {
|
|
112
|
+
backoff = RECONNECT_MIN_MS;
|
|
113
|
+
missedPongs = 0;
|
|
114
|
+
if (liveTimer)
|
|
115
|
+
clearInterval(liveTimer);
|
|
116
|
+
// A suspended Mac comes back with a socket that looks open and is not.
|
|
117
|
+
// Ping until two go unanswered, then tear it down so the normal backoff
|
|
118
|
+
// path reconnects - the daemon's own sleep detector reports the same
|
|
119
|
+
// suspends this is guarding against.
|
|
120
|
+
liveTimer = setInterval(() => {
|
|
121
|
+
if (sock.readyState !== ws_1.default.OPEN) {
|
|
122
|
+
sock.terminate();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (missedPongs >= 2) {
|
|
126
|
+
console.warn('[relay] no pong in 60s — assuming the link died, reconnecting');
|
|
127
|
+
sock.terminate();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
missedPongs += 1;
|
|
131
|
+
try {
|
|
132
|
+
sock.ping();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
sock.terminate();
|
|
136
|
+
}
|
|
137
|
+
}, PING_INTERVAL_MS);
|
|
138
|
+
});
|
|
139
|
+
sock.on('pong', () => { missedPongs = 0; });
|
|
103
140
|
sock.on('message', async (raw) => {
|
|
104
141
|
let msg;
|
|
105
142
|
try {
|
|
@@ -139,6 +176,10 @@ async function connect() {
|
|
|
139
176
|
}
|
|
140
177
|
});
|
|
141
178
|
const retry = () => {
|
|
179
|
+
if (liveTimer) {
|
|
180
|
+
clearInterval(liveTimer);
|
|
181
|
+
liveTimer = null;
|
|
182
|
+
}
|
|
142
183
|
if (ws !== sock)
|
|
143
184
|
return; // superseded by a newer socket
|
|
144
185
|
dropBridges();
|
|
@@ -161,6 +202,10 @@ function stopRelayClient() {
|
|
|
161
202
|
clearTimeout(idleTimer);
|
|
162
203
|
idleTimer = null;
|
|
163
204
|
}
|
|
205
|
+
if (liveTimer) {
|
|
206
|
+
clearInterval(liveTimer);
|
|
207
|
+
liveTimer = null;
|
|
208
|
+
}
|
|
164
209
|
dropBridges();
|
|
165
210
|
try {
|
|
166
211
|
ws?.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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": [
|