@addai/node 0.28.0 → 0.29.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 +10 -5
- package/dist/desktop/manager.d.ts +4 -0
- package/dist/desktop/manager.js +31 -2
- package/dist/desktop/spec.d.ts +4 -1
- package/package.json +1 -1
package/dist/command-runner.js
CHANGED
|
@@ -724,12 +724,17 @@ async function runDesktopCommand(cmd) {
|
|
|
724
724
|
break;
|
|
725
725
|
}
|
|
726
726
|
case 'desktop_start': {
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
727
|
+
// A full machine used to be a hard failure here, which left an entity
|
|
728
|
+
// with nothing to do but ask again and hope. It waits instead: the
|
|
729
|
+
// request keeps its place and the reconciler starts it when a slot
|
|
730
|
+
// frees. Said out loud, because a desktop that is "not starting yet"
|
|
731
|
+
// and one that is broken look identical from the outside.
|
|
730
732
|
const room = await (0, manager_1.desktopStartAllowed)(await (0, manager_1.listDesktops)(), row.id);
|
|
731
|
-
if (!room.allowed)
|
|
732
|
-
|
|
733
|
+
if (!room.allowed) {
|
|
734
|
+
await (0, manager_1.setStatus)(row.id, { status: 'queued', status_message: room.reason });
|
|
735
|
+
onLog(`${room.reason}\nWaiting for a free slot — this desktop keeps its place in the queue.\n`);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
733
738
|
await (0, manager_1.setStatus)(row.id, { status: 'starting' });
|
|
734
739
|
await provider.start(row);
|
|
735
740
|
await (0, manager_1.setStatus)(row.id, { status: 'running', status_message: null });
|
|
@@ -35,6 +35,10 @@ export declare function maxConcurrentDesktops(): number;
|
|
|
35
35
|
* tracked in a counter. A counter would drift the moment somebody stopped a
|
|
36
36
|
* container by hand, and drift upward means refusing to start anything. */
|
|
37
37
|
export declare function runningDesktopCount(rows: DesktopRow[]): Promise<number>;
|
|
38
|
+
/** Oldest waiter first, everything else after. Without this the order is
|
|
39
|
+
* whatever the listing happened to return, which is how a desktop ends up
|
|
40
|
+
* waiting behind one that asked after it. */
|
|
41
|
+
export declare function queueOrder<T extends Pick<DesktopRow, 'status' | 'queued_at'>>(rows: T[]): T[];
|
|
38
42
|
/** Whether one more may start. Returns the reason when it may not, so the
|
|
39
43
|
* caller can say something better than "failed". */
|
|
40
44
|
export declare function desktopStartAllowed(rows: DesktopRow[], excludeId?: string): Promise<{
|
package/dist/desktop/manager.js
CHANGED
|
@@ -43,6 +43,7 @@ exports.clampMaxDesktops = clampMaxDesktops;
|
|
|
43
43
|
exports.setMaxConcurrentDesktops = setMaxConcurrentDesktops;
|
|
44
44
|
exports.maxConcurrentDesktops = maxConcurrentDesktops;
|
|
45
45
|
exports.runningDesktopCount = runningDesktopCount;
|
|
46
|
+
exports.queueOrder = queueOrder;
|
|
46
47
|
exports.desktopStartAllowed = desktopStartAllowed;
|
|
47
48
|
exports.startDesktopManager = startDesktopManager;
|
|
48
49
|
exports.stopDesktopManager = stopDesktopManager;
|
|
@@ -68,6 +69,11 @@ function reconcileAction(row, actual) {
|
|
|
68
69
|
|| row.status === 'deleting' || row.status === 'pending'
|
|
69
70
|
|| row.status === 'failed')
|
|
70
71
|
return 'none';
|
|
72
|
+
// Waiting for a slot. The tick retries it every pass; whether there is room
|
|
73
|
+
// yet is decided there, because it depends on every OTHER desktop, not this
|
|
74
|
+
// one. Somebody starting it by hand in the meantime just resolves the wait.
|
|
75
|
+
if (row.status === 'queued')
|
|
76
|
+
return actual?.running ? 'mark_running' : 'start';
|
|
71
77
|
if (actual === null) {
|
|
72
78
|
// The row says this exists and it does not. Someone pruned it by hand.
|
|
73
79
|
return row.status === 'running' || row.status === 'stopped' ? 'mark_failed' : 'none';
|
|
@@ -176,6 +182,23 @@ async function runningDesktopCount(rows) {
|
|
|
176
182
|
}
|
|
177
183
|
return count;
|
|
178
184
|
}
|
|
185
|
+
/** When a desktop asked for its slot, as a sortable number. A waiter whose
|
|
186
|
+
* stamp is missing or unreadable goes to the BACK, not the front: Date.parse
|
|
187
|
+
* returns NaN for those, every NaN comparison is false, and a comparator that
|
|
188
|
+
* returns NaN is treated as 0 — so the unstamped row would quietly keep
|
|
189
|
+
* whatever position the listing gave it and cut in front of real waiters. */
|
|
190
|
+
function waitingSince(row) {
|
|
191
|
+
if (row.status !== 'queued')
|
|
192
|
+
return Number.POSITIVE_INFINITY;
|
|
193
|
+
const at = Date.parse(row.queued_at ?? '');
|
|
194
|
+
return Number.isFinite(at) ? at : Number.POSITIVE_INFINITY;
|
|
195
|
+
}
|
|
196
|
+
/** Oldest waiter first, everything else after. Without this the order is
|
|
197
|
+
* whatever the listing happened to return, which is how a desktop ends up
|
|
198
|
+
* waiting behind one that asked after it. */
|
|
199
|
+
function queueOrder(rows) {
|
|
200
|
+
return [...rows].sort((a, b) => waitingSince(a) - waitingSince(b));
|
|
201
|
+
}
|
|
179
202
|
/** Whether one more may start. Returns the reason when it may not, so the
|
|
180
203
|
* caller can say something better than "failed". */
|
|
181
204
|
async function desktopStartAllowed(rows, excludeId) {
|
|
@@ -199,7 +222,7 @@ async function tick() {
|
|
|
199
222
|
if (!provider)
|
|
200
223
|
return; // no engine: nothing to reconcile
|
|
201
224
|
const rows = await listDesktops();
|
|
202
|
-
for (const row of rows) {
|
|
225
|
+
for (const row of queueOrder(rows)) {
|
|
203
226
|
const actual = await provider.inspect(row);
|
|
204
227
|
const action = reconcileAction(row, actual);
|
|
205
228
|
if (action === 'none')
|
|
@@ -212,7 +235,13 @@ async function tick() {
|
|
|
212
235
|
// failing — nothing is wrong with the desktop, there is just no room.
|
|
213
236
|
const room = await desktopStartAllowed(rows, row.id);
|
|
214
237
|
if (!room.allowed) {
|
|
215
|
-
|
|
238
|
+
// Stay in the queue rather than dropping out of it. A desktop that
|
|
239
|
+
// fell back to 'stopped' here would need asking for all over again,
|
|
240
|
+
// which is exactly what waiting is supposed to save you.
|
|
241
|
+
// queued_at is stamped by the status RPC, which keeps the
|
|
242
|
+
// original on a re-queue — a desktop must not lose its place in
|
|
243
|
+
// the line every time the reconciler looks at it.
|
|
244
|
+
await setStatus(row.id, { status: 'queued', status_message: room.reason });
|
|
216
245
|
continue;
|
|
217
246
|
}
|
|
218
247
|
await provider.start(row);
|
package/dist/desktop/spec.d.ts
CHANGED
|
@@ -13,8 +13,11 @@ export interface DesktopRow {
|
|
|
13
13
|
enabled_skills: string[];
|
|
14
14
|
enabled_packages: string[];
|
|
15
15
|
autostart: boolean;
|
|
16
|
-
status: 'pending' | 'creating' | 'starting' | 'running' | 'stopped' | 'failed' | 'deleting';
|
|
16
|
+
status: 'pending' | 'creating' | 'starting' | 'running' | 'stopped' | 'failed' | 'deleting' | 'queued';
|
|
17
17
|
status_message: string | null;
|
|
18
|
+
/** Set while this desktop is waiting for a slot on a full machine. The node
|
|
19
|
+
* serves its queue oldest-first, so this is the place in the line. */
|
|
20
|
+
queued_at?: string | null;
|
|
18
21
|
vnc_port: number | null;
|
|
19
22
|
vnc_password: string | null;
|
|
20
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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": [
|