@runuai/host 0.9.157 → 0.9.159
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 +65 -10
- package/package.json +1 -1
- package/src/main.ts +18 -2
package/lib/orchestrator.ts
CHANGED
|
@@ -9374,10 +9374,54 @@ async function recoverPersistedTaskEnvironment(
|
|
|
9374
9374
|
return result.outcome === "recovered";
|
|
9375
9375
|
} catch (error) {
|
|
9376
9376
|
if (tunnelReadinessFailed) return false;
|
|
9377
|
+
if (error instanceof TaskRecoveryRowConflictError && isCurrent()) {
|
|
9378
|
+
return quarantineConflictingTask(task, environment, error, isCurrent, lifecycle);
|
|
9379
|
+
}
|
|
9377
9380
|
throw error;
|
|
9378
9381
|
}
|
|
9379
9382
|
}
|
|
9380
9383
|
|
|
9384
|
+
/** A durable row the provider can never reconcile with its locator. Retrying
|
|
9385
|
+
* cannot change the verdict, so the sweep must not treat it as an unknown
|
|
9386
|
+
* runtime state: that kept the whole runtime in checking, retrying every
|
|
9387
|
+
* ~50 s, while one task's row was wrong (2026-10-19). */
|
|
9388
|
+
export class TaskRecoveryRowConflictError extends Error {
|
|
9389
|
+
override name = "TaskRecoveryRowConflictError";
|
|
9390
|
+
}
|
|
9391
|
+
|
|
9392
|
+
/** Settle one task whose row conflicts with its locator without holding the
|
|
9393
|
+
* runtime: drain its consumers, stop the locator's environment, and mark the
|
|
9394
|
+
* row `error` so the task reads as failed rather than running. Only a stop
|
|
9395
|
+
* that cannot be proven throws on, keeping the sweep unresolved, because
|
|
9396
|
+
* then the runtime state really is unknown. Resume or Destroy clears it. */
|
|
9397
|
+
async function quarantineConflictingTask(
|
|
9398
|
+
task: typeof schema.hostTasks.$inferSelect,
|
|
9399
|
+
environment: TaskEnvironmentHandle<unknown>,
|
|
9400
|
+
conflict: TaskRecoveryRowConflictError,
|
|
9401
|
+
isCurrent: () => boolean,
|
|
9402
|
+
lifecycle?: Orchestrator,
|
|
9403
|
+
): Promise<boolean> {
|
|
9404
|
+
await lifecycle?.quarantineChannelForRecovery(task.taskId);
|
|
9405
|
+
if (!isCurrent()) return true;
|
|
9406
|
+
await environment.stop();
|
|
9407
|
+
const after = await environment.status();
|
|
9408
|
+
if (!isCurrent()) return true;
|
|
9409
|
+
if (after.state === "running" || after.state === "unknown") {
|
|
9410
|
+
throw new Error(
|
|
9411
|
+
`${conflict.message}, and its environment could not be proven stopped`,
|
|
9412
|
+
);
|
|
9413
|
+
}
|
|
9414
|
+
db_setStatus(task.taskId, "error", {
|
|
9415
|
+
codeServerPort: null,
|
|
9416
|
+
previewPorts: "[]",
|
|
9417
|
+
});
|
|
9418
|
+
clearTaskGatewayAcl(task.taskId);
|
|
9419
|
+
console.error(
|
|
9420
|
+
`[orchestrator] recovery: ${task.taskId} quarantined: ${conflict.message}; its environment was stopped and the task marked error so the runtime can go operational`,
|
|
9421
|
+
);
|
|
9422
|
+
return true;
|
|
9423
|
+
}
|
|
9424
|
+
|
|
9381
9425
|
interface RecoveryTaskConsumers {
|
|
9382
9426
|
quarantineChannelForRecovery(taskId: string): Promise<void>;
|
|
9383
9427
|
prepareChannelForRecovery(taskId: string): Promise<void>;
|
|
@@ -10020,23 +10064,28 @@ async function recoverDockerTaskEnvironment(
|
|
|
10020
10064
|
if (!task || !task.statusMirror || !isActive(task.statusMirror)) {
|
|
10021
10065
|
return { outcome: "recovered" };
|
|
10022
10066
|
}
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10067
|
+
// A starting/error row may hold only part of its legacy mirrors: a crash
|
|
10068
|
+
// before task-up finished leaves both NULL, and a start that failed after
|
|
10069
|
+
// an earlier recovery settled the row half-written leaves compose_project
|
|
10070
|
+
// set with worktree_path NULL (live 2026-10-19: one such row held the
|
|
10071
|
+
// whole runtime in checking for 2.5 h). Each mirror present must still
|
|
10072
|
+
// equal the locator; a missing one is adopted from it.
|
|
10073
|
+
const mayAdoptLocator =
|
|
10074
|
+
task.statusMirror === "starting" || task.statusMirror === "error";
|
|
10075
|
+
const mirrorAgrees = (mirror: string | null, expected: string): boolean =>
|
|
10076
|
+
mirror === expected || (mayAdoptLocator && mirror === null);
|
|
10027
10077
|
if (
|
|
10028
|
-
!
|
|
10029
|
-
(task.
|
|
10030
|
-
task.worktreePath !== locator.hostWorktreePath)
|
|
10078
|
+
!mirrorAgrees(task.composeProject, locator.composeProject) ||
|
|
10079
|
+
!mirrorAgrees(task.worktreePath, locator.hostWorktreePath)
|
|
10031
10080
|
) {
|
|
10032
|
-
throw new
|
|
10081
|
+
throw new TaskRecoveryRowConflictError(
|
|
10033
10082
|
`task ${descriptor.taskId} environment locator no longer matches its durable row`,
|
|
10034
10083
|
);
|
|
10035
10084
|
}
|
|
10036
10085
|
// Provider-owned provisional metadata is authoritative after a crash. Feed
|
|
10037
10086
|
// the already-validated deterministic values to the Docker-private repair
|
|
10038
10087
|
// helpers without teaching the generic sweep how to derive them.
|
|
10039
|
-
const recoveryTask =
|
|
10088
|
+
const recoveryTask = mayAdoptLocator
|
|
10040
10089
|
? {
|
|
10041
10090
|
...task,
|
|
10042
10091
|
composeProject: locator.composeProject,
|
|
@@ -10147,7 +10196,13 @@ async function recoverOneDockerTask(
|
|
|
10147
10196
|
db_setStatus(task.taskId, next, {
|
|
10148
10197
|
codeServerPort: null,
|
|
10149
10198
|
previewPorts: "[]",
|
|
10150
|
-
|
|
10199
|
+
// A prepared row reaches here with both mirrors adopted from its
|
|
10200
|
+
// locator: write them back as a pair. Writing compose_project alone
|
|
10201
|
+
// left a stopped row whose next failed Resume became the half-written
|
|
10202
|
+
// error row of 2026-10-19.
|
|
10203
|
+
...(next === "error"
|
|
10204
|
+
? { composeProject: null }
|
|
10205
|
+
: { composeProject, worktreePath: task.worktreePath }),
|
|
10151
10206
|
});
|
|
10152
10207
|
console.log(
|
|
10153
10208
|
`[orchestrator] recovery: ${task.taskId} -> ${next} (container gone, ` +
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -773,9 +773,13 @@ const credentialsActivation = new CredentialsActivationAuthority<CredentialsReti
|
|
|
773
773
|
log: (line) => console.warn(line),
|
|
774
774
|
});
|
|
775
775
|
setCredentialsModeSource(() => credentialsActivation.mode());
|
|
776
|
-
/** ADR-154 §13:
|
|
777
|
-
*
|
|
776
|
+
/** ADR-154 §13: the steady/operator cadence. Active runners report their
|
|
777
|
+
* purge again here too, so keep it deliberately quiet. */
|
|
778
778
|
const CREDENTIALS_ACTIVATION_RETRY_MS = 5 * 60_000;
|
|
779
|
+
/** A newly assigned marked machine is already behind its floor. Retry a
|
|
780
|
+
* transient freshness/Files proof inside Cloud's boot budget, without making
|
|
781
|
+
* its eventual active purge report a five-second heartbeat. */
|
|
782
|
+
const TASK_MACHINE_CREDENTIALS_ACTIVATION_RETRY_MS = 5_000;
|
|
779
783
|
const CREDENTIALS_ACTIVATION_ANSWER_MS = 60_000;
|
|
780
784
|
// Started here, first fired long after the bridge's state exists.
|
|
781
785
|
const credentialsActivationTimer = setInterval(() => {
|
|
@@ -788,6 +792,18 @@ const credentialsActivationTimer = setInterval(() => {
|
|
|
788
792
|
void credentialsActivation.advance();
|
|
789
793
|
}, CREDENTIALS_ACTIVATION_RETRY_MS);
|
|
790
794
|
credentialsActivationTimer.unref?.();
|
|
795
|
+
const taskMachineCredentialsActivationTimer =
|
|
796
|
+
taskMachineCredentialsRequired && sidecarTaskId() !== null
|
|
797
|
+
? setInterval(() => {
|
|
798
|
+
if (
|
|
799
|
+
stopping ||
|
|
800
|
+
!credentialFramesReady ||
|
|
801
|
+
credentialsMode() !== "pending"
|
|
802
|
+
) return;
|
|
803
|
+
void credentialsActivation.advance();
|
|
804
|
+
}, TASK_MACHINE_CREDENTIALS_ACTIVATION_RETRY_MS)
|
|
805
|
+
: null;
|
|
806
|
+
taskMachineCredentialsActivationTimer?.unref?.();
|
|
791
807
|
|
|
792
808
|
const credentialsActivationAsks = new Map<
|
|
793
809
|
string,
|