@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.
- package/lib/engine-login.ts +7 -1
- package/lib/managed-runtime.ts +70 -6
- package/package.json +1 -1
- package/src/index.ts +48 -2
- package/src/main.ts +26 -1
package/lib/engine-login.ts
CHANGED
|
@@ -1286,7 +1286,13 @@ export function engineLoginContainerArgs(
|
|
|
1286
1286
|
? [
|
|
1287
1287
|
"/usr/bin/script",
|
|
1288
1288
|
"-qefc",
|
|
1289
|
-
|
|
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"];
|
package/lib/managed-runtime.ts
CHANGED
|
@@ -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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|