@addai/node 0.30.5 → 0.30.6
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/desktop/manager.d.ts +6 -0
- package/dist/desktop/manager.js +79 -2
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -62,6 +62,12 @@ export declare function desktopStartAllowed(rows: DesktopRow[], excludeId?: stri
|
|
|
62
62
|
}>;
|
|
63
63
|
export declare function startDesktopManager(): void;
|
|
64
64
|
export declare function stopDesktopManager(): void;
|
|
65
|
+
/** For the sleep monitor. A machine that just resumed may have a Docker that
|
|
66
|
+
* died with the suspend and a cached provider still pointing at its dead
|
|
67
|
+
* socket — so drop the cache (the next probe re-detects, and relaunches the
|
|
68
|
+
* engine if it must) and reconcile now rather than up to a poll interval
|
|
69
|
+
* later. Safe any time: tick() no-ops if one is already running. */
|
|
70
|
+
export declare function reconcileAfterWake(): void;
|
|
65
71
|
/** Bring a desktop up and wait for it, for the run path. Returns the row when
|
|
66
72
|
* running, null when it could not be started - the caller then falls back to
|
|
67
73
|
* the host rather than failing the run. */
|
package/dist/desktop/manager.js
CHANGED
|
@@ -47,6 +47,7 @@ exports.queueOrder = queueOrder;
|
|
|
47
47
|
exports.desktopStartAllowed = desktopStartAllowed;
|
|
48
48
|
exports.startDesktopManager = startDesktopManager;
|
|
49
49
|
exports.stopDesktopManager = stopDesktopManager;
|
|
50
|
+
exports.reconcileAfterWake = reconcileAfterWake;
|
|
50
51
|
exports.ensureRunning = ensureRunning;
|
|
51
52
|
// Keeps the desktops the server believes in and the containers that actually
|
|
52
53
|
// exist in agreement. Modelled on projects.ts: a slow poll, no realtime, and
|
|
@@ -57,6 +58,7 @@ const supabase_client_1 = require("../supabase-client");
|
|
|
57
58
|
const store_1 = require("../store");
|
|
58
59
|
const spec_1 = require("./spec");
|
|
59
60
|
const docker_1 = require("./docker");
|
|
61
|
+
const start_engine_1 = require("./start-engine");
|
|
60
62
|
// Desktops are not latency-sensitive: a container that died is a rare event
|
|
61
63
|
// and 15s to notice it is fine. Slower interval = less pooler pressure.
|
|
62
64
|
const POLL_INTERVAL_MS = 15_000;
|
|
@@ -243,6 +245,63 @@ async function desktopStartAllowed(rows, excludeId) {
|
|
|
243
245
|
+ `its limit. Stop one, or raise the limit on the machine's page.`,
|
|
244
246
|
};
|
|
245
247
|
}
|
|
248
|
+
// ── Starting the engine when a desktop needs it ──────────────────────────────
|
|
249
|
+
//
|
|
250
|
+
// The commonest reason the Desktops tab is dark is Docker installed and closed
|
|
251
|
+
// — a fresh boot, or a laptop that resumed with Docker not relaunched. The
|
|
252
|
+
// reconcile used to see no engine and simply return, so a desktop that should
|
|
253
|
+
// be running sat dark until someone opened Docker by hand. Now, if any desktop
|
|
254
|
+
// on this machine wants to run and we can start the engine without a password
|
|
255
|
+
// (Windows/macOS), the daemon starts it itself and the next tick picks it up.
|
|
256
|
+
// Linux runs Docker as a root service and gets no unattended start here.
|
|
257
|
+
let engineStarting = false;
|
|
258
|
+
let lastEngineAttempt = 0;
|
|
259
|
+
const ENGINE_RETRY_MS = 3 * 60_000;
|
|
260
|
+
function wantsToRun(row) {
|
|
261
|
+
if (row.status === 'running' || row.status === 'queued')
|
|
262
|
+
return true;
|
|
263
|
+
return row.status === 'stopped' && !!row.autostart;
|
|
264
|
+
}
|
|
265
|
+
async function maybeStartEngineForDesktops() {
|
|
266
|
+
if (engineStarting || Date.now() - lastEngineAttempt < ENGINE_RETRY_MS)
|
|
267
|
+
return;
|
|
268
|
+
let rows;
|
|
269
|
+
try {
|
|
270
|
+
rows = await listDesktops();
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const waiting = rows.filter(wantsToRun);
|
|
276
|
+
if (waiting.length === 0)
|
|
277
|
+
return; // nothing on this machine needs the engine
|
|
278
|
+
// Only where we can drive it without a password. Linux needs systemctl and a
|
|
279
|
+
// root round trip we cannot answer unattended — and those hosts keep Docker
|
|
280
|
+
// running as a service anyway.
|
|
281
|
+
const plan = (0, start_engine_1.startPlan)(process.platform, (0, start_engine_1.runningAsRoot)(), process.env.ProgramFiles);
|
|
282
|
+
if (!plan.file || plan.needsRoot)
|
|
283
|
+
return;
|
|
284
|
+
engineStarting = true;
|
|
285
|
+
lastEngineAttempt = Date.now();
|
|
286
|
+
console.warn(`[desktops] the container engine is down and ${waiting.length} `
|
|
287
|
+
+ `desktop${waiting.length === 1 ? '' : 's'} want it — starting it`);
|
|
288
|
+
try {
|
|
289
|
+
const engine = await (0, start_engine_1.startEngine)(s => process.stdout.write(s), async () => null);
|
|
290
|
+
if (engine) {
|
|
291
|
+
(0, docker_1.resetProviderCache)();
|
|
292
|
+
console.log('[desktops] the container engine is up — reconciling');
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
console.warn('[desktops] the engine did not come up in time; will try again later');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
console.warn('[desktops] could not start the engine:', err.message);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
engineStarting = false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
246
305
|
let timer = null;
|
|
247
306
|
let inflight = false;
|
|
248
307
|
async function tick() {
|
|
@@ -250,9 +309,18 @@ async function tick() {
|
|
|
250
309
|
return;
|
|
251
310
|
inflight = true;
|
|
252
311
|
try {
|
|
253
|
-
|
|
312
|
+
let provider = await (0, docker_1.getProvider)();
|
|
313
|
+
// A provider cached as null from a boot when the engine was down stays null
|
|
314
|
+
// until forced — so re-probe once (cheap) in case the engine has since come
|
|
315
|
+
// up, by our hand or the user's.
|
|
254
316
|
if (!provider)
|
|
255
|
-
|
|
317
|
+
provider = await (0, docker_1.getProvider)(true);
|
|
318
|
+
if (!provider) {
|
|
319
|
+
// Still nothing. If a desktop wants the engine, start it; the next tick
|
|
320
|
+
// reconciles once it answers.
|
|
321
|
+
await maybeStartEngineForDesktops();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
256
324
|
const rows = await listDesktops();
|
|
257
325
|
for (const row of queueOrder(rows)) {
|
|
258
326
|
const actual = await provider.inspect(row);
|
|
@@ -320,6 +388,15 @@ function stopDesktopManager() {
|
|
|
320
388
|
timer = null;
|
|
321
389
|
}
|
|
322
390
|
}
|
|
391
|
+
/** For the sleep monitor. A machine that just resumed may have a Docker that
|
|
392
|
+
* died with the suspend and a cached provider still pointing at its dead
|
|
393
|
+
* socket — so drop the cache (the next probe re-detects, and relaunches the
|
|
394
|
+
* engine if it must) and reconcile now rather than up to a poll interval
|
|
395
|
+
* later. Safe any time: tick() no-ops if one is already running. */
|
|
396
|
+
function reconcileAfterWake() {
|
|
397
|
+
(0, docker_1.resetProviderCache)();
|
|
398
|
+
void tick();
|
|
399
|
+
}
|
|
323
400
|
/** Bring a desktop up and wait for it, for the run path. Returns the row when
|
|
324
401
|
* running, null when it could not be started - the caller then falls back to
|
|
325
402
|
* the host rather than failing the run. */
|
package/dist/index.js
CHANGED
|
@@ -200,9 +200,15 @@ async function start(argv = []) {
|
|
|
200
200
|
intervalMs: 30_000,
|
|
201
201
|
thresholdMs: 90_000, // >90s late = we were suspended (matches server offline window)
|
|
202
202
|
onWake: (gapMs) => {
|
|
203
|
-
console.warn(`[wake] detected ~${Math.round(gapMs / 1000)}s suspend — re-heartbeating and
|
|
203
|
+
console.warn(`[wake] detected ~${Math.round(gapMs / 1000)}s suspend — re-heartbeating, reclaiming runs, and rechecking desktops`);
|
|
204
204
|
void (0, heartbeat_1.beat)().catch(() => { });
|
|
205
205
|
reclaimOrphanedRequests('wake').catch(err => console.error('[wake] reclaim error:', err.message));
|
|
206
|
+
// A resumed laptop may have a Docker that went down with the suspend, and
|
|
207
|
+
// a desktop that should be running gone dark with it. Drop the cached
|
|
208
|
+
// provider (it may point at a dead socket) and reconcile now — which
|
|
209
|
+
// relaunches the engine if a desktop needs it — rather than waiting for
|
|
210
|
+
// the 15s poll to notice.
|
|
211
|
+
(0, manager_1.reconcileAfterWake)();
|
|
206
212
|
},
|
|
207
213
|
});
|
|
208
214
|
// Begin polling for pending requests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.6",
|
|
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": [
|