@addai/node 0.30.4 → 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/learn/browser.js +24 -1
- 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
|
@@ -171,10 +171,33 @@ function wrapperScript(realBrowser, extensionDir) {
|
|
|
171
171
|
const seed = extensionDir
|
|
172
172
|
? `[ -f ${exports.SEED_SCRIPT} ] && (node ${exports.SEED_SCRIPT} >/dev/null 2>&1 &)\n`
|
|
173
173
|
: '';
|
|
174
|
+
// ⚠️ A lock that now outlives the machine that made it.
|
|
175
|
+
//
|
|
176
|
+
// Chrome writes SingletonLock as a symlink to "<hostname>-<pid>" and refuses
|
|
177
|
+
// to open a profile locked by somebody else. That was harmless while the
|
|
178
|
+
// profile lived in the container's writable layer, because a rebuild
|
|
179
|
+
// destroyed the lock along with everything else. Putting the profile on the
|
|
180
|
+
// mount to make it SURVIVE means the lock survives too — and a container's
|
|
181
|
+
// hostname is its id, which changes every rebuild. So the next container
|
|
182
|
+
// finds a lock owned by a machine that no longer exists and shows
|
|
183
|
+
//
|
|
184
|
+
// The profile appears to be in use by another Google Chrome process
|
|
185
|
+
// (674) on another computer (7fd0c26e0fdf)
|
|
186
|
+
//
|
|
187
|
+
// instead of the page it was asked for. Persistence brought this with it;
|
|
188
|
+
// it is the cost of the same change, and it needed paying here.
|
|
189
|
+
//
|
|
190
|
+
// Guarded on nothing being alive, which is what makes this safe rather than
|
|
191
|
+
// reckless: two Chromes on one profile corrupts it. If one IS running, the
|
|
192
|
+
// lock is real and must stand — the singleton then does its actual job and
|
|
193
|
+
// hands the url to the running browser, which is the behaviour we want.
|
|
194
|
+
const unlock = `pgrep -f '${realBrowser}' >/dev/null 2>&1 || `
|
|
195
|
+
+ `rm -f ${PROFILE_DIR}/SingletonLock ${PROFILE_DIR}/SingletonCookie `
|
|
196
|
+
+ `${PROFILE_DIR}/SingletonSocket\n`;
|
|
174
197
|
return `#!/bin/sh
|
|
175
198
|
# Written by @addai/node so +Ai Learn can watch this browser${extensionDir ? ' and the +Ai Vault can fill logins in it' : ''}.
|
|
176
199
|
# Removing it costs you the page detail in a lesson${extensionDir ? ' and vault autofill' : ''} and nothing else.
|
|
177
|
-
${seed}exec ${realBrowser} --user-data-dir=${PROFILE_DIR} --remote-debugging-port=${exports.DEBUG_PORT} --remote-debugging-address=127.0.0.1${FIRST_RUN}${ext} "$@"
|
|
200
|
+
${unlock}${seed}exec ${realBrowser} --user-data-dir=${PROFILE_DIR} --remote-debugging-port=${exports.DEBUG_PORT} --remote-debugging-address=127.0.0.1${FIRST_RUN}${ext} "$@"
|
|
178
201
|
`;
|
|
179
202
|
}
|
|
180
203
|
/** Is what is on disk already what we would write? Exact match, so a change to
|
|
@@ -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": [
|