@runuai/host 0.9.168 → 0.9.170

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.
@@ -74,6 +74,7 @@ import type {
74
74
  TaskProjectStatusResult,
75
75
  } from "../src/protocol";
76
76
  import { RUNTIME_AUTHORITY_ENV } from "./runtime-authority";
77
+ import { TaskEnvironmentRequestValidationError } from "./task-environment/types";
77
78
  import type {
78
79
  TaskEnvironmentExecRequest,
79
80
  TaskEnvironmentExecResult,
@@ -271,7 +272,7 @@ export class TaskProjectJournalStore {
271
272
 
272
273
  /**
273
274
  * $1 staging repo path, $2 target path, $3 repo URL, $4 task branch,
274
- * $5 activation id, $6 the project's fallback `.tool-versions` ("" for none).
275
+ * $5 activation id, optional $6 the project's fallback `.tool-versions`.
275
276
  * Exit 3: the target already exists. The last stdout line is the checked-out
276
277
  * commit, or `empty` for a repository with no commits.
277
278
  */
@@ -684,13 +685,13 @@ export class TaskProjectOps {
684
685
  }
685
686
 
686
687
  prepare(input: TaskProjectPrepareInput): Promise<TaskProjectPrepareResult> {
687
- return this.serialized(input.taskId, async () => {
688
- if (!OPERATION_ID.test(input.operationId)) throw new Error("invalid operation id");
689
- if (!SLUG.test(input.project.slug)) throw new Error("invalid project slug");
688
+ return this.serialized<TaskProjectPrepareResult>(input.taskId, async () => {
689
+ if (!OPERATION_ID.test(input.operationId)) throw new TaskEnvironmentRequestValidationError("invalid operation id");
690
+ if (!SLUG.test(input.project.slug)) throw new TaskEnvironmentRequestValidationError("invalid project slug");
690
691
  if (!REPO_URL.test(input.project.repoUrl)) {
691
- return refused<TaskProjectPrepareResult>("clone_failed", "the repository URL is not an HTTPS or SSH Git remote");
692
+ throw new TaskEnvironmentRequestValidationError("the repository URL is not an HTTPS or SSH Git remote");
692
693
  }
693
- if (!BRANCH.test(input.branch)) throw new Error("invalid task branch");
694
+ if (!BRANCH.test(input.branch)) throw new TaskEnvironmentRequestValidationError("invalid task branch");
694
695
  const fenced = this.fenceFails(input);
695
696
  if (fenced) return refused<TaskProjectPrepareResult>(fenced.reason, fenced.message);
696
697
 
@@ -728,7 +729,9 @@ export class TaskProjectOps {
728
729
  input.project.repoUrl,
729
730
  input.branch,
730
731
  activationId,
731
- input.project.toolVersions ?? "",
732
+ // The provider rejects empty argv elements. The script already
733
+ // defaults an absent final argument to no fallback (${6:-}).
734
+ ...(input.project.toolVersions ? [input.project.toolVersions] : []),
732
735
  ],
733
736
  { cwd: environment.descriptor.workspacePath, timeoutMs: TASK_PROJECT_CLONE_TIMEOUT_MS },
734
737
  ),
@@ -780,6 +783,13 @@ export class TaskProjectOps {
780
783
  updatedAt: Date.now(),
781
784
  });
782
785
  return { outcome: "prepared", head, envSchema };
786
+ }).catch((error: unknown) => {
787
+ if (error instanceof TaskEnvironmentRequestValidationError) {
788
+ return refused<TaskProjectPrepareResult>("invalid_request", error.message);
789
+ }
790
+ // Provider availability, rebuilds and journal I/O remain INTERNAL so
791
+ // the cloud can retry them; never classify an error by its message.
792
+ throw error;
783
793
  });
784
794
  }
785
795
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.168",
3
+ "version": "0.9.170",
4
4
  "description": "Uai host \u2014 runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
package/src/main.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import type { AssignmentDelivery, DeliveryOrigin } from "./protocol";
3
3
 
4
4
  import "./load-env";
5
+ import { sampleDiskPressure } from "../lib/disk-pressure";
5
6
  import { TASK_SECRETS_FEATURE, isTaskSecretStagingPut } from "./task-secrets";
6
7
  import { writeTaskSecretStaging } from "../lib/task-secret-staging";
7
8
 
@@ -1473,12 +1474,28 @@ async function startLocalUi(): Promise<void> {
1473
1474
  }
1474
1475
  }
1475
1476
 
1477
+ // Capacity fits the existing capability/status snapshot; no new messages.
1478
+ let diskSnapshot: HostCapabilities["disk"];
1479
+ let diskSampling = false;
1480
+ async function refreshDiskSnapshot(): Promise<void> {
1481
+ if (diskSampling || sidecarMode()) return;
1482
+ diskSampling = true;
1483
+ try {
1484
+ diskSnapshot = await sampleDiskPressure();
1485
+ sendCapabilities();
1486
+ } finally { diskSampling = false; }
1487
+ }
1488
+ void refreshDiskSnapshot();
1489
+ const diskTimer = setInterval(() => void refreshDiskSnapshot(), 60_000);
1490
+ diskTimer.unref?.();
1491
+
1476
1492
  /** Build the current host capability advertisement (ADR-021). */
1477
1493
  function buildCapabilities(): HostCapabilities {
1478
1494
  const runtime = containerRuntimeCapabilityState();
1479
1495
  const projectEnvBindings = projectEnvBindingCapability();
1480
1496
  return {
1481
1497
  version: packageVersion(),
1498
+ ...(diskSnapshot ? { disk: diskSnapshot } : {}),
1482
1499
  protocolFeatures: [
1483
1500
  "host-workspace-file-read-v1",
1484
1501
  ASSIGNMENT_EVIDENCE_PROTOCOL_FEATURE,
package/src/protocol.ts CHANGED
@@ -4068,6 +4068,15 @@ export function isHostConfigAckFrame(
4068
4068
  * changes. The cloud caches it per hostId to drive the task-creation pickers.
4069
4069
  */
4070
4070
  export interface HostCapabilities {
4071
+ /** Filesystem capacity, refreshed each minute. Null means unmeasurable
4072
+ * (for example Docker in a VM); available excludes root-reserved blocks. */
4073
+ disk?: {
4074
+ sampledAt: number;
4075
+ /** Cloud-only: overwritten on authenticated receipt, never trusted from the host. */
4076
+ receivedAt?: number;
4077
+ home: { totalBytes: number; availableBytes: number } | null;
4078
+ docker: { totalBytes: number; availableBytes: number } | null;
4079
+ };
4071
4080
  /** The host-agent package version — the cloud UI shows it on the host page
4072
4081
  * and flags when npm has a newer release. Optional (older hosts omit it). */
4073
4082
  version?: string;
@@ -4337,6 +4346,15 @@ export interface TaskDiffInput {
4337
4346
  projects: Array<{ id: string; slug: string }>;
4338
4347
  }
4339
4348
 
4349
+ /** Cloud Prepare retry timing. */
4350
+ export const TASK_PROJECT_PREPARE_MAX_ATTEMPTS = 12;
4351
+ export const TASK_PROJECT_PREPARE_TIMEOUT_MS = 11 * 60_000;
4352
+ export function taskProjectRetryDelayMs(attempts: number): number {
4353
+ return Math.min(5 * 60_000, 2_000 * 2 ** Math.min(attempts, 7));
4354
+ }
4355
+ /** Bound the CLI tool call; the durable cloud operation may outlast it. */
4356
+ export const TASK_PROJECT_ADD_WAIT_MS = 40 * 60_000;
4357
+
4340
4358
  /**
4341
4359
  * ADR-161 §2.3: every live-project command names the task's run, the
4342
4360
  * operation, and the project-set revision the operation commits. The runner
@@ -4362,6 +4380,7 @@ export interface TaskProjectPrepareInput extends TaskProjectOpFence {
4362
4380
  /** Why a live-project command did nothing. `grant_not_installed` and
4363
4381
  * `busy` are retryable; the rest are final for that operation. */
4364
4382
  export type TaskProjectRefusal =
4383
+ | "invalid_request"
4365
4384
  | "repo_access_denied"
4366
4385
  | "clone_failed"
4367
4386
  | "workspace_path_taken"