@runuai/host 0.8.24 → 0.8.26

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/agent.ts CHANGED
@@ -178,9 +178,13 @@ async function runAgent<T extends z.ZodTypeAny>(
178
178
  if (parsed) {
179
179
  const failure = FailureEnvelope.safeParse(parsed);
180
180
  if (failure.success) {
181
+ // Prefer the actual tool output over the bash envelope's generic
182
+ // "agent step failed" — the docker/git/apt error is what the operator
183
+ // needs, and it must reach the cloud, not just the host log.
184
+ const detail = toolStderrDetail(stderrBuf);
181
185
  throw new AgentError(
182
186
  failure.data.error.code,
183
- failure.data.error.message,
187
+ detail || failure.data.error.message,
184
188
  {
185
189
  step: failure.data.error.step,
186
190
  exitCode: failure.data.error.exit_code ?? exitCode,
@@ -236,6 +240,28 @@ function tryParseLastJsonLine(buf: string): unknown {
236
240
  return null;
237
241
  }
238
242
 
243
+ /**
244
+ * The FAILING TOOL's output from a script's stderr — the docker/git/apt lines
245
+ * that actually say WHY a step failed, dropping our own `[uai-agent] step:`
246
+ * breadcrumbs and the `{"ok":false,…}` envelope. The bash envelope's message
247
+ * is generic ("agent step failed"); this is what makes it specific so the
248
+ * reason reaches the cloud UI instead of only the host log — useless when the
249
+ * host is a remote user's machine (live 2026-07-22, "compose_up_failed" with
250
+ * no why). Bounded to the last few lines so a build log doesn't flood the UI.
251
+ */
252
+ export function toolStderrDetail(stderr: string): string {
253
+ const lines = stderr
254
+ .split(/\r?\n/)
255
+ .map((l) => l.replace(/\s+$/, ""))
256
+ .filter((l) => l.trim().length > 0)
257
+ .filter((l) => !l.startsWith("[uai-agent"))
258
+ .filter((l) => !l.trimStart().startsWith('{"ok"'));
259
+ return lines
260
+ .slice(-6)
261
+ .map((l) => (l.length > 300 ? `${l.slice(0, 300)}…` : l))
262
+ .join("\n");
263
+ }
264
+
239
265
  // ---------------------------------------------------------------------------
240
266
  // Public API. One function per agent command.
241
267
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.8.24",
3
+ "version": "0.8.26",
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>",
@@ -322,6 +322,11 @@ fi
322
322
  printf ' image: %s\n' "$DERIVED_IMAGE"
323
323
  else
324
324
  printf ' image: %s\n' "$STANDARD_IMAGE"
325
+ # Never pull: the standard image is built LOCALLY by the host, not hosted
326
+ # on any registry. Without this, an absent image makes compose attempt a
327
+ # doomed registry pull ("pull access denied for uai-standard") instead of
328
+ # failing clearly (live 2026-07-22, a fresh host mid standard-image build).
329
+ printf ' pull_policy: never\n'
325
330
  fi
326
331
  printf ' init: true\n'
327
332
  printf ' working_dir: /workspace\n'
@@ -441,6 +446,18 @@ fi
441
446
  docker volume create "$ASDF_VOLUME" >/dev/null 2>&1 || true
442
447
  docker volume create "$PW_VOLUME" >/dev/null 2>&1 || true
443
448
 
449
+ # The standard base image is built by the host (ensureStandardImage), never
450
+ # pulled from a registry — and derived tasks FROM it too. If it's absent (the
451
+ # host started before Docker was ready, or the build failed) compose would try
452
+ # a doomed registry pull; fail with an actionable message instead (the host
453
+ # also (re)builds it on demand before task-up — this is the belt-and-suspenders
454
+ # check for a still-in-progress or failed build). Live 2026-07-22.
455
+ if ! docker image inspect "$STANDARD_IMAGE" >/dev/null 2>&1; then
456
+ emit_err "STANDARD_IMAGE_MISSING" \
457
+ "the host base image $STANDARD_IMAGE is not built yet — the host builds it at startup (a few minutes on first run, and after Docker Desktop finishes starting). Wait for the host log line 'built standard image', or restart the host, then retry. If it keeps failing the host log shows the build error (often Docker Desktop low on memory/disk)." \
458
+ "check standard image"
459
+ fi
460
+
444
461
  step "COMPOSE_UP_FAILED" "docker compose up -d"
445
462
  if [ "$has_derived" = "1" ]; then
446
463
  docker compose -p "$compose_project" -f "$task_uai_dir/docker-compose.yml" up -d --build >/dev/null
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { agent, AgentError } from "../lib/agent";
2
+ import { ensureStandardImage } from "../lib/standard-image";
2
3
  import { cloneRepo } from "../lib/repo-clone";
3
4
  import { handleFilesOp } from "../lib/shared-files";
4
5
  import { getOrchestrator } from "../lib/orchestrator";
@@ -119,6 +120,15 @@ export const hostCommands: HostCommands = {
119
120
  storeTaskCliSecret(input.task.id, input.task.cliSecret);
120
121
  }
121
122
  recordHostEvent(input.task.id, "task.created");
123
+ // Self-heal the standard base image before task-up needs it. The boot-time
124
+ // build is best-effort + one-shot: if the host started before Docker was
125
+ // ready (common on Docker Desktop), uai-standard:dev was never built and
126
+ // every task-up died on a doomed registry pull (live 2026-07-22). This is
127
+ // idempotent — a fast image-inspect when present, a real build only when
128
+ // missing/stale (Docker is definitely up now, a task just arrived). Never
129
+ // throws; a build failure surfaces as a clear STANDARD_IMAGE_MISSING from
130
+ // task-up.sh's guard.
131
+ await ensureStandardImage();
122
132
  const result = await wrapAgent(ctx, "taskUp", () => agent.taskUp(input));
123
133
  if (result.ok) {
124
134
  recordTaskUpResult(input.task.id, result.value);