@runuai/host 0.9.129 → 0.9.131

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.
@@ -293,6 +293,12 @@ interface Channel {
293
293
  /** Agents with a reconcile-spawn in flight (ADR-049 mid-task adds) — guards
294
294
  * against a concurrent ensure double-spawning the same new agent. */
295
295
  spawning: Set<string>;
296
+ /** Roster agents that have bound at least once on this channel. The launch
297
+ * screen's `ready` counts these, not the live `sessions` map: a one-shot
298
+ * engine (grok, cursor) exits seconds after its bind, so "every roster
299
+ * agent bound at this instant" was never true for a mixed roster after a
300
+ * wake — live 2026-09-10, task machine, 4 agents, screen up for good. */
301
+ everBound: Set<string>;
296
302
  /** Factory creates are externally side-effecting before they bind. Teardown
297
303
  * drains them so an old channel cannot resume against a reused container
298
304
  * name and overwrite the replacement generation's durable transport. */
@@ -717,6 +723,7 @@ export class Orchestrator {
717
723
  mcpConfigAppliedEpoch: 0,
718
724
  configGeneration: 0,
719
725
  spawning: new Set(),
726
+ everBound: new Set(),
720
727
  factoryOperations: new Set(),
721
728
  eventOperations: new Set(),
722
729
  accountByAgent: new Map(),
@@ -1631,19 +1638,30 @@ export class Orchestrator {
1631
1638
 
1632
1639
 
1633
1640
  /** Install a session with an event handler scoped to this exact generation. */
1641
+ /** Every roster agent has bound at least once — the launch screen lifts. */
1642
+ private rosterReady(channel: Channel): boolean {
1643
+ return channel.roster.every(
1644
+ (agent) => channel.sessions.has(agent.id) || channel.everBound.has(agent.id),
1645
+ );
1646
+ }
1647
+
1634
1648
  private bindSession(
1635
1649
  channel: Channel,
1636
1650
  agentId: string,
1637
1651
  session: AgentSession,
1638
1652
  ): void {
1639
1653
  channel.sessions.set(agentId, session);
1654
+ channel.everBound.add(agentId);
1640
1655
  // The launch screen's `ready` rides the bind, not the tail of the start
1641
1656
  // loop: whichever path brings the last roster agent up — the initial
1642
1657
  // start, a reconcile that won the race with it, a respawn after Resume —
1643
1658
  // is the moment the task can take a message. Live 2026-09-09: `agents`
1644
1659
  // was stored, Jonah was up and idle, and no `ready` ever followed, so
1645
1660
  // the page stayed covered for minutes. Idempotent on the cloud side.
1646
- if (channel.roster.every((agent) => channel.sessions.has(agent.id))) {
1661
+ // Counted on `everBound`, not `sessions`: an agent whose engine exits
1662
+ // after each turn leaves the map seconds after binding and must not hold
1663
+ // the screen up for the rest of the roster (live 2026-09-10).
1664
+ if (this.rosterReady(channel)) {
1647
1665
  this.emitTaskStage(channel.taskId, "ready");
1648
1666
  }
1649
1667
  // Adapters may synchronously emit `error` followed by `turn_complete` (or
@@ -1990,7 +2008,9 @@ export class Orchestrator {
1990
2008
  // this unconditional `agents` then overwrote it — the page stayed
1991
2009
  // covered for good. Only announce the wake-up when there is one to do.
1992
2010
  const allBound = (): boolean =>
1993
- initialRoster.every((agent) => channel.sessions.has(agent.id));
2011
+ initialRoster.every(
2012
+ (agent) => channel.sessions.has(agent.id) || channel.everBound.has(agent.id),
2013
+ );
1994
2014
  if (!allBound()) this.emitTaskStage(channel.taskId, "agents");
1995
2015
  for (const agent of initialRoster) {
1996
2016
  if (!this.isActiveChannel(channel)) return false;
@@ -2055,7 +2075,11 @@ export class Orchestrator {
2055
2075
  // already bound when this loop began — every bind then predates this
2056
2076
  // pass and could have been overwritten by an `agents` from a start
2057
2077
  // that lost the race.
2058
- if (allBound()) this.emitTaskStage(channel.taskId, "ready");
2078
+ // Every roster agent was attempted above; if anything is up the task can
2079
+ // take a message, whether or not a one-shot engine has already exited.
2080
+ if (allBound() || channel.sessions.size > 0) {
2081
+ this.emitTaskStage(channel.taskId, "ready");
2082
+ }
2059
2083
  return true;
2060
2084
  }
2061
2085
 
@@ -287,13 +287,22 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
287
287
  ) {
288
288
  throw new Error("self copy paths must be absolute and NUL-free");
289
289
  }
290
- // Both sides are this machine. `cp -a` keeps directory trees intact
291
- // (engine-account homes), and the result is chowned to the task user so
292
- // the agents can read what was copied in for them.
290
+ // Both sides are this machine. A directory source lands as the
291
+ // destination's CONTENTS, the way the machine provider's tar stream
292
+ // does: engine-account provisioning pre-creates the destination, and a
293
+ // plain `cp -a dir dest` into an existing dest nests `dest/dir/…`, so
294
+ // CODEX_HOME / GROK_HOME pointed at a dir without auth.json and both
295
+ // engines ran signed out and silent (live 2026-09-10, first codex + grok
296
+ // agents on a task machine). A file source copies as before. The result
297
+ // is chowned to the task user when this process is root, so the agents
298
+ // can read what was copied in for them.
299
+ const copy =
300
+ 'if [ -d "$1" ]; then mkdir -p "$2" && cp -a "$1/." "$2/"; ' +
301
+ 'else mkdir -p "$(dirname "$2")" && cp -a "$1" "$2"; fi';
293
302
  const script =
294
- request.direction === "into"
295
- ? 'mkdir -p "$(dirname "$2")" && cp -a "$1" "$2" && chown -R node:node "$2"'
296
- : 'mkdir -p "$(dirname "$2")" && cp -a "$1" "$2"';
303
+ request.direction === "into" && this.#privileged()
304
+ ? `${copy} && chown -R ${SELF_TASK_USER}:${SELF_TASK_USER} "$2"`
305
+ : copy;
297
306
  const result = await this.exec({
298
307
  argv: ["/bin/sh", "-c", script, "copy", request.source, request.destination],
299
308
  user: "root",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.129",
3
+ "version": "0.9.131",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",