@runuai/host 0.9.43 → 0.9.45

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.
@@ -1286,7 +1286,13 @@ export function engineLoginContainerArgs(
1286
1286
  ? [
1287
1287
  "/usr/bin/script",
1288
1288
  "-qefc",
1289
- "exec claude setup-token",
1289
+ // A wide PTY first: `script` allocates 80×24 when stdin is no
1290
+ // terminal, the CLI hard-wraps the OAuth URL at that width, and
1291
+ // the streamed capture then sees only the first fragment — the
1292
+ // operator lands on claude.ai with "Missing redirect_uri" and half
1293
+ // a client id (live 2026-08-18). stty targets the PTY `script`
1294
+ // just allocated.
1295
+ "stty cols 500 rows 50 2>/dev/null; exec claude setup-token",
1290
1296
  "/dev/null",
1291
1297
  ]
1292
1298
  : ["codex", "login"];
@@ -806,12 +806,62 @@ export function managedRuntimeActivationPending(home?: string): boolean {
806
806
  * taskUp and must be repaired by a safe uninstall retry.
807
807
  */
808
808
  export function managedRuntimeAdmissionPending(home?: string): boolean {
809
+ return managedRuntimeAdmissionBlocker(home) !== null;
810
+ }
811
+
812
+ /** The exact file closing task admission, or null. Refusals must NAME their
813
+ * gate: three stacked silent refusals cost a live operator hours
814
+ * (2026-08-18) because nothing anywhere said which file to look at. */
815
+ export function managedRuntimeAdmissionBlocker(home?: string): string | null {
809
816
  const paths = managedRuntimePaths(home);
810
- return (
811
- pathExistsSync(paths.activationPending) ||
812
- pathExistsSync(paths.maintenancePending) ||
813
- pathExistsSync(paths.installIntent)
814
- );
817
+ for (const marker of [
818
+ paths.activationPending,
819
+ paths.maintenancePending,
820
+ paths.installIntent,
821
+ ]) {
822
+ if (pathExistsSync(marker)) return marker;
823
+ }
824
+ return null;
825
+ }
826
+
827
+ /**
828
+ * Boot-time self-heal for the recurring silent-gate incident (live
829
+ * 2026-08-11 and twice on 2026-08-18): an uninstall/update helper that died
830
+ * at phase "checking" leaves its admission gate behind forever, and every
831
+ * taskUp / channelEnsure then bounces retryably with no feedback anywhere.
832
+ *
833
+ * A "checking" marker is reclaimable by the flow's OWN contract — no
834
+ * teardown has begun at that phase (removeManagedRuntime reopens admission
835
+ * itself on an ordinary busy refusal) — and a marker observed while holding
836
+ * the operation lock cannot belong to a live operation. Later phases
837
+ * (teardown/cleanup) and unreadable markers stay fail-closed: state may be
838
+ * half-removed and only an uninstall retry may touch them.
839
+ */
840
+ export async function reclaimAbandonedMaintenanceCheck(
841
+ home?: string,
842
+ ): Promise<"cleared" | "kept" | "none"> {
843
+ const paths = managedRuntimePaths(home);
844
+ if (!pathExistsSync(paths.maintenancePending)) return "none";
845
+ let release: () => Promise<void>;
846
+ try {
847
+ release = await acquireManagedOperationLock(paths);
848
+ } catch {
849
+ // A live install/update/uninstall owns the gate; leave it alone.
850
+ return "kept";
851
+ }
852
+ try {
853
+ let marker: MaintenanceMarker | null;
854
+ try {
855
+ marker = await readMaintenanceMarker(paths);
856
+ } catch {
857
+ return "kept";
858
+ }
859
+ if (!marker || marker.phase !== "checking") return "kept";
860
+ await removeDurably(paths.maintenancePending);
861
+ return "cleared";
862
+ } finally {
863
+ await release();
864
+ }
815
865
  }
816
866
 
817
867
  /**
@@ -1448,7 +1498,21 @@ export async function removeManagedRuntime(
1448
1498
  // before inspecting SQLite; taskUp writes `starting` before its final gate
1449
1499
  // check. Whichever arrives second observes the first, so teardown cannot
1450
1500
  // begin while an unrecorded task proceeds into Docker side effects.
1451
- if (!(await options.canRemove())) {
1501
+ let removable: boolean;
1502
+ try {
1503
+ removable = await options.canRemove();
1504
+ } catch (error) {
1505
+ if (marker.phase === "checking") {
1506
+ // The check itself failed, so no teardown began — leaving the gate
1507
+ // behind here poisoned every taskUp/channelEnsure with a silent
1508
+ // retryable refusal until an operator found the file (live
1509
+ // 2026-08-18, twice). Same safety argument as the busy refusal
1510
+ // below: reopen admission, keep the error.
1511
+ await removeDurably(paths.maintenancePending);
1512
+ }
1513
+ throw error;
1514
+ }
1515
+ if (!removable) {
1452
1516
  if (marker.phase === "checking") {
1453
1517
  // No teardown began, so this ordinary busy refusal may safely reopen
1454
1518
  // admission. A retry of a previously-started teardown stays fail-closed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.43",
3
+ "version": "0.9.45",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",
package/src/index.ts CHANGED
@@ -75,7 +75,10 @@ import {
75
75
  } from "../lib/preview-sidecar";
76
76
  import { inspectTunnelContainer } from "../lib/tunnel-runtime";
77
77
  import { ContainerRuntimeUnavailableError } from "../lib/runtime-guard";
78
- import { managedRuntimeAdmissionPending } from "../lib/managed-runtime";
78
+ import {
79
+ managedRuntimeAdmissionBlocker,
80
+ reclaimAbandonedMaintenanceCheck,
81
+ } from "../lib/managed-runtime";
79
82
  import { retireTaskSshIdentityEnsures } from "../lib/git-identity";
80
83
  import { removeTaskIdentityStrict } from "../lib/ssh";
81
84
  import { proveOrphanTaskResourcesAbsent } from "../lib/task-inventory";
@@ -1346,8 +1349,51 @@ function boundedFailureMessage(error: unknown): string {
1346
1349
  }
1347
1350
 
1348
1351
  /** Managed installation, activation, and uninstall drain instead of admitting work. */
1352
+ let lastAdmissionRefusalLogAt = 0;
1353
+ let lastAdmissionReclaimAt = 0;
1354
+ let admissionReclaimInFlight: Promise<"cleared" | "kept" | "none"> | null =
1355
+ null;
1349
1356
  function managedMaintenanceUnavailable(): HostCommandResult<never> | null {
1350
- if (!managedRuntimeAdmissionPending()) return null;
1357
+ const blocker = managedRuntimeAdmissionBlocker();
1358
+ if (blocker === null) return null;
1359
+ const now = Date.now();
1360
+ // Mid-run self-heal (the abandoned-uninstall marker recurred WHILE the
1361
+ // service was up, 2026-08-19 — boot-time reclaim alone cannot cover a
1362
+ // writer on a schedule). Reclaim only ever touches a provably-untorn
1363
+ // "checking" marker, under the operation lock; rate-limited so the
1364
+ // cloud's retry hammer cannot churn the lock. The command that observed
1365
+ // the marker still refuses — the NEXT retry finds admission open.
1366
+ if (now - lastAdmissionReclaimAt > 30_000 && !admissionReclaimInFlight) {
1367
+ lastAdmissionReclaimAt = now;
1368
+ admissionReclaimInFlight = reclaimAbandonedMaintenanceCheck();
1369
+ void admissionReclaimInFlight
1370
+ .then((verdict) => {
1371
+ if (verdict === "cleared") {
1372
+ console.warn(
1373
+ "[host-agent] cleared an abandoned maintenance marker mid-run (a managed uninstall/update died before any teardown); task admission reopened",
1374
+ );
1375
+ }
1376
+ })
1377
+ .catch((error) => {
1378
+ console.warn(
1379
+ `[host-agent] mid-run maintenance-marker reclaim failed: ${
1380
+ error instanceof Error ? error.message : String(error)
1381
+ }`,
1382
+ );
1383
+ })
1384
+ .finally(() => {
1385
+ admissionReclaimInFlight = null;
1386
+ });
1387
+ }
1388
+ // A refusal must NAME its gate somewhere an operator can find it. Three
1389
+ // stacked silent refusals cost hours live (2026-08-18); rate-limited so a
1390
+ // retrying cloud cannot flood the log.
1391
+ if (now - lastAdmissionRefusalLogAt > 60_000) {
1392
+ lastAdmissionRefusalLogAt = now;
1393
+ console.warn(
1394
+ `[host-agent] refusing task admission: managed-maintenance marker present at ${blocker}`,
1395
+ );
1396
+ }
1351
1397
  return {
1352
1398
  ok: false,
1353
1399
  code: HostErrorCode.HostUnavailable,
package/src/main.ts CHANGED
@@ -127,7 +127,11 @@ import {
127
127
  onChange as onRegistryChange,
128
128
  } from "../lib/agents/registry";
129
129
  import { canAdvertiseTypedSecretaryDispatch } from "../lib/agents/mode";
130
- import { MANAGED_UPDATE_RESTART_EXIT_CODE } from "../lib/managed-runtime";
130
+ import {
131
+ MANAGED_UPDATE_RESTART_EXIT_CODE,
132
+ managedRuntimeAdmissionBlocker,
133
+ reclaimAbandonedMaintenanceCheck,
134
+ } from "../lib/managed-runtime";
131
135
  import { ManagedOperationDrain } from "../lib/managed-operation-drain";
132
136
  import {
133
137
  createHostLogManager,
@@ -371,6 +375,27 @@ interface PausableSource {
371
375
  }
372
376
 
373
377
  console.log(`[host-agent] starting host ${hostId}`);
378
+ // Self-heal the recurring silent admission gate BEFORE anything can refuse
379
+ // work: an uninstall/update helper that died at phase "checking" (live
380
+ // 2026-08-11 + twice 2026-08-18) left every taskUp/channelEnsure bouncing
381
+ // retryably with no feedback. Only a provably-untorn "checking" marker is
382
+ // reclaimed; anything else stays fail-closed and is NAMED in the log.
383
+ try {
384
+ const reclaim = await reclaimAbandonedMaintenanceCheck();
385
+ if (reclaim === "cleared") {
386
+ console.warn(
387
+ "[host-agent] cleared an abandoned maintenance marker (a managed uninstall/update died before any teardown); task admission reopened",
388
+ );
389
+ } else if (reclaim === "kept") {
390
+ console.warn(
391
+ `[host-agent] task admission is CLOSED by a managed-maintenance marker: ${managedRuntimeAdmissionBlocker() ?? "(marker path unavailable)"} — retry the pending uninstall/update or remove the marker to reopen`,
392
+ );
393
+ }
394
+ } catch (error) {
395
+ console.warn(
396
+ `[host-agent] abandoned-maintenance reclaim failed: ${error instanceof Error ? error.message : String(error)}`,
397
+ );
398
+ }
374
399
  migrateHostDb();
375
400
  // Reconcile a durable retention-off setting before recovery/connect can make
376
401
  // crash-left owner-facing terminal history observable again. Correctness