@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.
@@ -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. */
@@ -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
- const provider = await (0, docker_1.getProvider)();
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
- return; // no engine: nothing to reconcile
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 reclaiming in-flight runs`);
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.5",
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": [