@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.
- package/lib/orchestrator.ts +27 -3
- package/lib/task-environment/self.ts +15 -6
- package/package.json +1 -1
package/lib/orchestrator.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
291
|
-
//
|
|
292
|
-
//
|
|
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
|
-
?
|
|
296
|
-
:
|
|
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",
|