@runuai/host 0.9.42 → 0.9.43

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.
@@ -111,7 +111,10 @@ import {
111
111
  type ResolvedEngineAccount,
112
112
  } from "./engine-accounts";
113
113
  import { clearTaskGatewayAcl, setupMcpTaskConfig } from "./mcp-gateway";
114
- import { stopPreviewSidecars } from "./preview-sidecar";
114
+ import {
115
+ PREVIEW_SIDECAR_TASK_LABEL,
116
+ stopPreviewSidecars,
117
+ } from "./preview-sidecar";
115
118
  import {
116
119
  agentClisReady,
117
120
  standardImageCorepackVersionPath,
@@ -4110,8 +4113,12 @@ async function dockerStart(containerName: string): Promise<DockerStartResult> {
4110
4113
  return "failed";
4111
4114
  }
4112
4115
 
4116
+ // The fourth column is the preview-sidecar marker: a sidecar can wear the
4117
+ // task's compose labels, and a row that is neither app-shaped nor a known
4118
+ // sidecar reads as AMBIGUOUS here — which fenced maintenance on a healthy
4119
+ // host (live 2026-08-18).
4113
4120
  const RUNTIME_WRITER_LIST_FORMAT =
4114
- '{{json .Names}}\t{{.Label "com.docker.compose.project"}}\t{{.Label "com.docker.compose.service"}}';
4121
+ `{{json .Names}}\t{{.Label "com.docker.compose.project"}}\t{{.Label "com.docker.compose.service"}}\t{{.Label "${PREVIEW_SIDECAR_TASK_LABEL}"}}`;
4115
4122
  const RUNTIME_WRITER_INSPECT_FORMAT = [
4116
4123
  "{{json .Id}}",
4117
4124
  "{{json .Name}}",
@@ -4233,10 +4240,12 @@ async function listRuntimeWriterCandidates(): Promise<
4233
4240
  for (const rawLine of listed.stdout.split(/\r?\n/)) {
4234
4241
  if (!rawLine) continue;
4235
4242
  const fields = rawLine.split("\t");
4236
- if (fields.length !== 3) {
4243
+ if (fields.length !== 4) {
4237
4244
  ambiguous = true;
4238
4245
  continue;
4239
4246
  }
4247
+ // A preview sidecar is a known non-writer, not an ambiguity.
4248
+ if (fields[3] !== "") continue;
4240
4249
  let name: unknown;
4241
4250
  try {
4242
4251
  name = JSON.parse(fields[0]!);
@@ -21,7 +21,17 @@ import { inspectTunnelContainer } from "./tunnel-runtime";
21
21
 
22
22
  /** Port the proxy listens on inside the sidecar (published to a random host port). */
23
23
  const INNER_PORT = 9000;
24
- const LABEL = "com.runuai.preview-sidecar.task";
24
+ /**
25
+ * Exported for every "which container is the task's APP" scan: sidecars can
26
+ * legitimately wear the task's compose project/service labels (teardown
27
+ * finds them by project), so any container carrying THIS label must be
28
+ * excluded before counting app containers. Live 2026-08-18: one browser
29
+ * sidecar with compose labels made legacy adoption count two app containers
30
+ * and fail the whole runtime selection closed — every task on the host read
31
+ * as "still starting".
32
+ */
33
+ export const PREVIEW_SIDECAR_TASK_LABEL = "com.runuai.preview-sidecar.task";
34
+ const LABEL = PREVIEW_SIDECAR_TASK_LABEL;
25
35
  const TARGET_LABEL = "com.runuai.preview-sidecar.target";
26
36
 
27
37
  /** (taskId,name,containerPort) -> published 127.0.0.1 host port of a live sidecar. */
@@ -16,6 +16,7 @@ import {
16
16
  } from "../agent";
17
17
  import { dockerCli, type DockerResult } from "../docker-exec";
18
18
  import { taskDir } from "../env";
19
+ import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
19
20
  import { assertSafeHostTaskId } from "../task-identity";
20
21
  import {
21
22
  assertTaskEnvironmentProcessRequest,
@@ -868,8 +869,12 @@ function taskStatusToEnvironmentStatus(
868
869
  };
869
870
  }
870
871
 
872
+ // The fourth column is the preview-sidecar marker: a sidecar wearing the
873
+ // task's compose labels must read as "not the app", not as malformed
874
+ // identity data — the latter turned a healthy running task's status into
875
+ // "unknown" (live 2026-08-18).
871
876
  const DOCKER_STATUS_FORMAT =
872
- '{{.Names}}\t{{.Label "com.docker.compose.project"}}\t{{.State}}';
877
+ `{{.Names}}\t{{.Label "com.docker.compose.project"}}\t{{.State}}\t{{.Label "${PREVIEW_SIDECAR_TASK_LABEL}"}}`;
873
878
 
874
879
  async function inspectDockerTaskEnvironmentStatus(
875
880
  docker: typeof dockerCli,
@@ -897,14 +902,20 @@ async function inspectDockerTaskEnvironmentStatus(
897
902
 
898
903
  let state: string | undefined;
899
904
  for (const raw of result.stdout.split(/\r?\n/)) {
900
- const line = raw.trim();
901
- if (!line) continue;
902
- const [name, project, candidateState, ...extra] = line.split("\t");
905
+ // No trim before splitting: the app row's LAST column (the sidecar
906
+ // marker) is empty, and trimming would eat its tab and shift the shape.
907
+ if (!raw.trim()) continue;
908
+ const [name, project, candidateState, sidecar, ...extra] = raw.split("\t");
909
+ // A preview sidecar wearing the task's compose labels is not the app
910
+ // container; skipping it keeps a healthy task from reading as unknown.
911
+ if (sidecar !== undefined && sidecar !== "") continue;
903
912
  if (
904
913
  state !== undefined ||
905
914
  name !== environment.containerName ||
906
915
  project !== environment.composeProject ||
907
916
  !candidateState ||
917
+ !candidateState.trim() ||
918
+ sidecar === undefined ||
908
919
  extra.length > 0
909
920
  ) {
910
921
  return {
@@ -5,6 +5,7 @@ import { eq } from "drizzle-orm";
5
5
  import { dockerCli, type DockerResult } from "../docker-exec";
6
6
  import { getDb, schema, type HostTask } from "../db";
7
7
  import { parsePreviewPortRuntimes } from "../preview-ports";
8
+ import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
8
9
  import { assertSafeHostTaskId } from "../task-identity";
9
10
  import {
10
11
  APPLE_TASK_ENVIRONMENT_PROVIDER,
@@ -26,7 +27,11 @@ const INSPECT_OUTPUT_LIMIT = 128 * 1024;
26
27
  const INVENTORY_OUTPUT_LIMIT = 128 * 1024;
27
28
  const LEGACY_INSPECT_FORMAT =
28
29
  '{{json .Id}}\t{{json .Name}}\t{{json (index .Config.Labels "com.docker.compose.project")}}\t{{json (index .Config.Labels "com.docker.compose.service")}}\t{{json .State.Status}}\t{{json .Mounts}}';
29
- const LEGACY_INVENTORY_FORMAT = "{{.ID}}\t{{.Names}}";
30
+ // The third column is the preview-sidecar marker: a sidecar can wear the
31
+ // task's compose project/service labels (teardown finds it by project), so
32
+ // the app-container census must see and skip it — counting one as an app
33
+ // container failed the WHOLE runtime adoption closed (live 2026-08-18).
34
+ const LEGACY_INVENTORY_FORMAT = `{{.ID}}\t{{.Names}}\t{{.Label "${PREVIEW_SIDECAR_TASK_LABEL}"}}`;
30
35
 
31
36
  type LegacyTaskRow = Pick<
32
37
  HostTask,
@@ -317,27 +322,38 @@ async function proveNoConflictingAppContainer(
317
322
  ) {
318
323
  throw new Error(`task ${candidate.row.taskId} app inventory failed`);
319
324
  }
325
+ if (result.stdout !== "" && !result.stdout.endsWith("\n")) {
326
+ throw new Error(`task ${candidate.row.taskId} app inventory failed`);
327
+ }
328
+ const appRows: string[][] = [];
329
+ for (const line of result.stdout === ""
330
+ ? []
331
+ : result.stdout.slice(0, -1).split("\n")) {
332
+ const fields = line.split("\t");
333
+ if (fields.length !== 3) {
334
+ throw new Error(`task ${candidate.row.taskId} app inventory failed`);
335
+ }
336
+ // A preview sidecar wearing the task's compose labels is not an app
337
+ // container; counting one here failed the whole runtime adoption closed
338
+ // (live 2026-08-18, six labeled browser sidecars on the Studio).
339
+ if (fields[2] !== "") continue;
340
+ appRows.push(fields);
341
+ }
320
342
  if (expected === null) {
321
343
  // The exact container is proven absent; adoption is sound only if no
322
- // same-project replacement exists either (an empty inventory prints
323
- // nothing at all).
324
- if (result.stdout !== "") {
344
+ // same-project replacement exists either.
345
+ if (appRows.length !== 0) {
325
346
  throw new Error(
326
347
  `task ${candidate.row.taskId} has conflicting app containers`,
327
348
  );
328
349
  }
329
350
  return;
330
351
  }
331
- if (!result.stdout.endsWith("\n")) {
332
- throw new Error(`task ${candidate.row.taskId} app inventory failed`);
333
- }
334
- const lines = result.stdout.slice(0, -1).split("\n");
335
- if (lines.length !== 1) {
352
+ if (appRows.length !== 1) {
336
353
  throw new Error(`task ${candidate.row.taskId} has conflicting app containers`);
337
354
  }
338
- const fields = lines[0]!.split("\t");
355
+ const fields = appRows[0]!;
339
356
  if (
340
- fields.length !== 2 ||
341
357
  fields[0] !== expected.containerId ||
342
358
  fields[1] !== expected.containerName
343
359
  ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.42",
3
+ "version": "0.9.43",
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>",