@addai/node 0.30.5 → 0.30.7

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/dist/win.js CHANGED
@@ -245,6 +245,28 @@ function resolveNpmShimScript(shimPath) {
245
245
  const re = /%(?:~dp0|dp0%)[\\/]([^"\s]+\.(?:m|c)?js)/gi;
246
246
  for (const m of body.matchAll(re))
247
247
  candidates.push(m[1]);
248
+ // ⚠️ The CLI entry point first — NOT the file's order.
249
+ //
250
+ // npm's own npm.cmd names two %~dp0 scripts, and the helper comes first:
251
+ //
252
+ // SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js"
253
+ // SET "NPM_CLI_JS=%~dp0\node_modules\npm\bin\npm-cli.js"
254
+ // ...
255
+ // "%NODE_EXE%" "%NPM_CLI_JS%" %*
256
+ //
257
+ // Only that last line runs, and it runs NPM_CLI_JS. Taking the first that
258
+ // exists therefore spawned npm-prefix.js — and the reason this hid for so
259
+ // long is that doing so does not FAIL: it exits 0 and prints the npm prefix
260
+ // path. npmLatestVersion saw a clean exit whose output was not a version,
261
+ // returned null, and the auto-updater logged "could not read the latest
262
+ // published version" on every tick. A Windows node then sat for ever on the
263
+ // version it was installed with — the machine this was found on was still
264
+ // running 0.30.3 while 0.30.6 was published.
265
+ //
266
+ // Every package bin shim names exactly one script and is unaffected: the
267
+ // sort is stable, so with nothing to promote nothing moves.
268
+ const isCliEntry = (rel) => /(^|[\\/])(npm|npx)-cli\.(m|c)?js$/i.test(rel);
269
+ candidates.sort((a, b) => Number(isCliEntry(b)) - Number(isCliEntry(a)));
248
270
  for (const rel of candidates) {
249
271
  // Shims always write backslashes. Split on either separator and re-join
250
272
  // with the platform's, so the same parsing is exercisable off Windows —
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "@addai/node",
3
- "version": "0.30.5",
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
- "license": "MIT",
6
- "keywords": [
7
- "addai",
8
- "entity-studio",
9
- "claude",
10
- "codex",
11
- "kimi",
12
- "gemini",
13
- "agent",
14
- "daemon",
15
- "supabase",
16
- "mcp"
17
- ],
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/just-AddAi/addai-entity-runtime.git"
21
- },
22
- "homepage": "https://github.com/just-AddAi/addai-entity-runtime#readme",
23
- "bugs": {
24
- "url": "https://github.com/just-AddAi/addai-entity-runtime/issues"
25
- },
26
- "type": "commonjs",
27
- "main": "dist/index.js",
28
- "bin": {
29
- "ainode": "dist/cli.js"
30
- },
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "files": [
35
- "dist",
36
- "scripts",
37
- "README.md",
38
- "assets"
39
- ],
40
- "scripts": {
41
- "build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
42
- "start": "node dist/cli.js",
43
- "dev": "tsc -p tsconfig.json && node dist/cli.js",
44
- "test": "npm run build && node --test test/*.test.mjs",
45
- "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
46
- "postinstall": "node scripts/fix-pty-helper.js",
47
- "prepublishOnly": "npm run build"
48
- },
49
- "engines": {
50
- "node": ">=18"
51
- },
52
- "dependencies": {
53
- "@addai/node-flows": "^1.0.0",
54
- "node-pty": "^1.1.0",
55
- "ws": "^8.21.3"
56
- },
57
- "devDependencies": {
58
- "@types/node": "^20.0.0",
59
- "@types/ws": "^8.18.1",
60
- "typescript": "^5.6.0"
61
- }
62
- }
1
+ {
2
+ "name": "@addai/node",
3
+ "version": "0.30.7",
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
+ "license": "MIT",
6
+ "keywords": [
7
+ "addai",
8
+ "entity-studio",
9
+ "claude",
10
+ "codex",
11
+ "kimi",
12
+ "gemini",
13
+ "agent",
14
+ "daemon",
15
+ "supabase",
16
+ "mcp"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/just-AddAi/addai-entity-runtime.git"
21
+ },
22
+ "homepage": "https://github.com/just-AddAi/addai-entity-runtime#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/just-AddAi/addai-entity-runtime/issues"
25
+ },
26
+ "type": "commonjs",
27
+ "main": "dist/index.js",
28
+ "bin": {
29
+ "ainode": "dist/cli.js"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "scripts",
37
+ "README.md",
38
+ "assets"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
42
+ "start": "node dist/cli.js",
43
+ "dev": "tsc -p tsconfig.json && node dist/cli.js",
44
+ "test": "npm run build && node --test test/*.test.mjs",
45
+ "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
46
+ "postinstall": "node scripts/fix-pty-helper.js",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "dependencies": {
53
+ "@addai/node-flows": "^1.0.0",
54
+ "node-pty": "^1.1.0",
55
+ "ws": "^8.21.3"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.0.0",
59
+ "@types/ws": "^8.18.1",
60
+ "typescript": "^5.6.0"
61
+ }
62
+ }