@runuai/host 0.8.18 → 0.8.19
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/lib/orchestrator.ts +53 -12
- package/package.json +1 -1
package/lib/orchestrator.ts
CHANGED
|
@@ -1463,7 +1463,12 @@ async function dockerListContainersByLabel(
|
|
|
1463
1463
|
{ timeoutMs: 60_000 },
|
|
1464
1464
|
);
|
|
1465
1465
|
if (res.status === null) return null; // docker state UNKNOWN — don't act on it
|
|
1466
|
-
|
|
1466
|
+
// Non-zero is ALSO unknown, not "no containers": after a machine reboot the
|
|
1467
|
+
// host service races the Docker daemon, and `docker ps` fails fast with
|
|
1468
|
+
// "cannot connect". Treating that as [] downgraded every active task to
|
|
1469
|
+
// stopped on boot (live user report 2026-07-21) — the exited-container
|
|
1470
|
+
// restart branch below never got a chance to run.
|
|
1471
|
+
if (res.status !== 0) return null;
|
|
1467
1472
|
return res.stdout
|
|
1468
1473
|
.split("\n")
|
|
1469
1474
|
.map((l) => l.trim())
|
|
@@ -1518,7 +1523,40 @@ async function dockerExec(
|
|
|
1518
1523
|
}
|
|
1519
1524
|
|
|
1520
1525
|
/** Exported for tests; production entry is the getOrchestrator() boot guard. */
|
|
1521
|
-
export async function recoverRunningTasks(
|
|
1526
|
+
export async function recoverRunningTasks(opts?: {
|
|
1527
|
+
/** Delay between passes while docker is unreachable (tests shrink it). */
|
|
1528
|
+
retryMs?: number;
|
|
1529
|
+
/** Bound on passes; ~15 min at the default cadence covers slow boots. */
|
|
1530
|
+
maxPasses?: number;
|
|
1531
|
+
}): Promise<void> {
|
|
1532
|
+
const retryMs = opts?.retryMs ?? 20_000;
|
|
1533
|
+
const maxPasses = opts?.maxPasses ?? 45;
|
|
1534
|
+
// After a machine reboot the host service and the Docker daemon start
|
|
1535
|
+
// concurrently; a pass that finds docker unreachable resolves nothing and
|
|
1536
|
+
// must retry once the daemon is up — the whole point of recovery is to
|
|
1537
|
+
// restart the exited containers that reboot left behind.
|
|
1538
|
+
for (let pass = 1; ; pass++) {
|
|
1539
|
+
const deferred = await recoveryPass();
|
|
1540
|
+
if (deferred === 0) return;
|
|
1541
|
+
if (pass >= maxPasses) {
|
|
1542
|
+
console.warn(
|
|
1543
|
+
`[orchestrator] recovery: docker still unreachable after ${pass} passes — giving up (${deferred} task(s) unresolved)`,
|
|
1544
|
+
);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (pass === 1) {
|
|
1548
|
+
console.log(
|
|
1549
|
+
`[orchestrator] recovery: docker not ready — retrying every ${Math.round(retryMs / 1000)}s for ${deferred} task(s)`,
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/** One recovery sweep. Returns how many tasks were deferred on an
|
|
1557
|
+
* unreachable docker (0 = everything resolved one way or another). */
|
|
1558
|
+
async function recoveryPass(): Promise<number> {
|
|
1559
|
+
let deferred = 0;
|
|
1522
1560
|
try {
|
|
1523
1561
|
const db = getDb();
|
|
1524
1562
|
const rows = db
|
|
@@ -1526,13 +1564,13 @@ export async function recoverRunningTasks(): Promise<void> {
|
|
|
1526
1564
|
.from(schema.hostTasks)
|
|
1527
1565
|
.where(inArray(schema.hostTasks.statusMirror, [...ACTIVE_STATUSES]))
|
|
1528
1566
|
.all();
|
|
1529
|
-
if (rows.length === 0) return;
|
|
1567
|
+
if (rows.length === 0) return 0;
|
|
1530
1568
|
console.log(
|
|
1531
1569
|
`[orchestrator] recovery: scanning ${rows.length} active task row(s)`,
|
|
1532
1570
|
);
|
|
1533
1571
|
for (const task of rows) {
|
|
1534
1572
|
try {
|
|
1535
|
-
await recoverOneTask(task);
|
|
1573
|
+
if (!(await recoverOneTask(task))) deferred += 1;
|
|
1536
1574
|
} catch (err) {
|
|
1537
1575
|
console.error(
|
|
1538
1576
|
`[orchestrator] recovery: ${task.taskId} failed:`,
|
|
@@ -1546,17 +1584,19 @@ export async function recoverRunningTasks(): Promise<void> {
|
|
|
1546
1584
|
err instanceof Error ? err.message : err,
|
|
1547
1585
|
);
|
|
1548
1586
|
}
|
|
1587
|
+
return deferred;
|
|
1549
1588
|
}
|
|
1550
1589
|
|
|
1590
|
+
/** Returns false when docker was unreachable (caller retries the pass). */
|
|
1551
1591
|
async function recoverOneTask(
|
|
1552
1592
|
task: typeof schema.hostTasks.$inferSelect,
|
|
1553
|
-
): Promise<
|
|
1593
|
+
): Promise<boolean> {
|
|
1554
1594
|
const composeProject = task.composeProject;
|
|
1555
1595
|
if (!composeProject) {
|
|
1556
1596
|
// We never wrote a compose project name for this row — must be
|
|
1557
1597
|
// a row stuck in `queued`/`starting` from before task-up got past
|
|
1558
1598
|
// step 2. Leave it; the user can recreate.
|
|
1559
|
-
return;
|
|
1599
|
+
return true;
|
|
1560
1600
|
}
|
|
1561
1601
|
const containerName = `${composeProject}-app-1`;
|
|
1562
1602
|
const containers = await dockerListContainersByLabel(
|
|
@@ -1564,11 +1604,11 @@ async function recoverOneTask(
|
|
|
1564
1604
|
);
|
|
1565
1605
|
if (containers === null) {
|
|
1566
1606
|
// Docker unreachable / timed out — actual state unknown. Leave the row
|
|
1567
|
-
// alone; the
|
|
1607
|
+
// alone; the retrying caller re-scans once docker answers.
|
|
1568
1608
|
console.warn(
|
|
1569
|
-
`[orchestrator] recovery: ${task.taskId}
|
|
1609
|
+
`[orchestrator] recovery: ${task.taskId} deferred — docker did not answer`,
|
|
1570
1610
|
);
|
|
1571
|
-
return;
|
|
1611
|
+
return false;
|
|
1572
1612
|
}
|
|
1573
1613
|
|
|
1574
1614
|
if (containers.length === 0) {
|
|
@@ -1585,7 +1625,7 @@ async function recoverOneTask(
|
|
|
1585
1625
|
`[orchestrator] recovery: ${task.taskId} -> ${next} (container gone, ` +
|
|
1586
1626
|
`worktree ${workspaceOnDisk ? "present" : "gone"})`,
|
|
1587
1627
|
);
|
|
1588
|
-
return;
|
|
1628
|
+
return true;
|
|
1589
1629
|
}
|
|
1590
1630
|
|
|
1591
1631
|
const running = containers.some((c) => c.State === "running");
|
|
@@ -1607,7 +1647,7 @@ async function recoverOneTask(
|
|
|
1607
1647
|
if (task.ownerUserId) {
|
|
1608
1648
|
void setupTaskGithub(task.taskId, task.ownerUserId);
|
|
1609
1649
|
}
|
|
1610
|
-
return;
|
|
1650
|
+
return true;
|
|
1611
1651
|
}
|
|
1612
1652
|
|
|
1613
1653
|
// Container exists but exited. Bring it back up + re-launch the
|
|
@@ -1620,7 +1660,7 @@ async function recoverOneTask(
|
|
|
1620
1660
|
codeServerPort: null,
|
|
1621
1661
|
previewPorts: "[]",
|
|
1622
1662
|
});
|
|
1623
|
-
return;
|
|
1663
|
+
return true;
|
|
1624
1664
|
}
|
|
1625
1665
|
// Correctly-owned Codex creds BEFORE uai-init / any agent respawn: the boot
|
|
1626
1666
|
// reinject sweep only targets containers already running, so a container
|
|
@@ -1649,6 +1689,7 @@ async function recoverOneTask(
|
|
|
1649
1689
|
console.log(
|
|
1650
1690
|
`[orchestrator] recovery: ${task.taskId} resumed (port ${port ?? "?"})`,
|
|
1651
1691
|
);
|
|
1692
|
+
return true;
|
|
1652
1693
|
}
|
|
1653
1694
|
|
|
1654
1695
|
function db_setStatus(
|
package/package.json
CHANGED