@runuai/host 0.9.115 → 0.9.117
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/env.ts +4 -0
- package/lib/orchestrator.ts +9 -0
- package/lib/standard-image.ts +41 -8
- package/lib/task-environment/index.ts +1 -1
- package/package.json +1 -1
- package/src/index.ts +12 -1
- package/src/main.ts +12 -0
- package/src/protocol.ts +10 -0
package/lib/env.ts
CHANGED
|
@@ -57,6 +57,10 @@ const schema = z.object({
|
|
|
57
57
|
// ADR-135: the broker Lambda a Uai-operated host invokes for a reactive
|
|
58
58
|
// refresh (name or ARN). Unset = scheduled refreshes only.
|
|
59
59
|
UAI_TOKEN_BROKER_FUNCTION: z.string().min(1).optional(),
|
|
60
|
+
// Who keeps the standard image and agent CLIs current. `host` (owned hosts):
|
|
61
|
+
// this process, hourly. `external` (Uai-operated hosts): a cloud job keeps
|
|
62
|
+
// the payload AMI fresh and this host only reconciles once at start.
|
|
63
|
+
UAI_IMAGE_MAINTENANCE: z.enum(["host", "external"]).default("host"),
|
|
60
64
|
NODE_ENV: z
|
|
61
65
|
.enum(["development", "production", "test"])
|
|
62
66
|
.default("development"),
|
package/lib/orchestrator.ts
CHANGED
|
@@ -135,6 +135,7 @@ import type {
|
|
|
135
135
|
HostEvent,
|
|
136
136
|
} from "../src/protocol";
|
|
137
137
|
import {
|
|
138
|
+
type TaskStartStage,
|
|
138
139
|
HOST_ACCOUNT_SCOPE,
|
|
139
140
|
MAX_AGENT_ID_CHARS,
|
|
140
141
|
MAX_SECRETARY_DISPATCH_ID_CHARS,
|
|
@@ -455,6 +456,11 @@ export class Orchestrator {
|
|
|
455
456
|
this.emitHost({ kind: "system.note", taskId, text });
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
/** A task-start stage boundary (the launch screen on the task page). */
|
|
460
|
+
emitTaskStage(taskId: string, stage: TaskStartStage): void {
|
|
461
|
+
this.emitHost({ kind: "task.stage", taskId, stage, at: Date.now() });
|
|
462
|
+
}
|
|
463
|
+
|
|
458
464
|
private emitHost(event: HostEvent): void {
|
|
459
465
|
for (const fn of this.hostSubscribers) {
|
|
460
466
|
try {
|
|
@@ -1889,6 +1895,7 @@ export class Orchestrator {
|
|
|
1889
1895
|
// which the client caps at 10s (Codex) / 30s (Claude Code). Idempotent
|
|
1890
1896
|
// and ~0.5s once the build is present; a failure arms its own 60s retry
|
|
1891
1897
|
// rather than depending on another delivery/ensure to arrive.
|
|
1898
|
+
this.emitTaskStage(channel.taskId, "tools");
|
|
1892
1899
|
await this.ensureBrowserSetup(
|
|
1893
1900
|
channel,
|
|
1894
1901
|
initialRoster.some((a) => a.kind === "codex"),
|
|
@@ -1923,6 +1930,7 @@ export class Orchestrator {
|
|
|
1923
1930
|
return false;
|
|
1924
1931
|
}
|
|
1925
1932
|
|
|
1933
|
+
this.emitTaskStage(channel.taskId, "agents");
|
|
1926
1934
|
for (const agent of initialRoster) {
|
|
1927
1935
|
if (!this.isActiveChannel(channel)) return false;
|
|
1928
1936
|
// A prior partial start or an independently guarded recovery may already
|
|
@@ -1972,6 +1980,7 @@ export class Orchestrator {
|
|
|
1972
1980
|
// so it both duplicated the opening turn AND re-injected it on a host
|
|
1973
1981
|
// restart, making agents redo the first turn mid-task. The persona/mission
|
|
1974
1982
|
// an agent needs live in the always-on system preamble, so nothing is lost.
|
|
1983
|
+
this.emitTaskStage(channel.taskId, "ready");
|
|
1975
1984
|
return true;
|
|
1976
1985
|
}
|
|
1977
1986
|
|
package/lib/standard-image.ts
CHANGED
|
@@ -1128,21 +1128,54 @@ function markAgentClisReady(): void {
|
|
|
1128
1128
|
* `{ ok, error? }` so the caller decides (boot: fire-and-forget; taskUp:
|
|
1129
1129
|
* surface the reason). Resolves `agentClisReady` on every exit path.
|
|
1130
1130
|
*/
|
|
1131
|
-
export function ensureStandardImage(
|
|
1131
|
+
export function ensureStandardImage(
|
|
1132
|
+
opts: { maxAgeMs?: number } = {},
|
|
1133
|
+
): Promise<StandardImageResult> {
|
|
1134
|
+
// A task-up passes maxAgeMs: a pass that succeeded recently for the same
|
|
1135
|
+
// engine configuration is reused instead of spending ~15 s spinning a
|
|
1136
|
+
// maintenance container per task (live 2026-09-09: 16 s of every task
|
|
1137
|
+
// start). The hourly schedule (main.ts) and the boot pass keep it fresh.
|
|
1138
|
+
const engines = configuredOptionalEngines();
|
|
1139
|
+
const key = optionalEngineConfigurationKey(engines);
|
|
1140
|
+
const recent = lastOkByKey.get(key);
|
|
1141
|
+
if (
|
|
1142
|
+
opts.maxAgeMs !== undefined &&
|
|
1143
|
+
recent !== undefined &&
|
|
1144
|
+
Date.now() - recent.at <= opts.maxAgeMs
|
|
1145
|
+
) {
|
|
1146
|
+
markAgentClisReady();
|
|
1147
|
+
return Promise.resolve(recent.result);
|
|
1148
|
+
}
|
|
1132
1149
|
// Startup, runtime recovery, and a newly-arrived task can all discover the
|
|
1133
1150
|
// same daemon transition. One shared volume repair/build must own that
|
|
1134
1151
|
// window; concurrent maintenance containers can otherwise replace the same
|
|
1135
1152
|
// CLI shims underneath each other and defeat agentClisReady's fence.
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1153
|
+
return ensureQueue.run(key, () =>
|
|
1154
|
+
ensureStandardImageInner(engines)
|
|
1155
|
+
.then((result) => {
|
|
1156
|
+
if (result.ok) lastOkByKey.set(key, { at: Date.now(), result });
|
|
1157
|
+
else lastOkByKey.delete(key);
|
|
1158
|
+
return result;
|
|
1159
|
+
})
|
|
1160
|
+
.finally(() => {
|
|
1161
|
+
// Always signal — even a docker-down bail — so agent spawns never hang
|
|
1162
|
+
// waiting on a reconcile that will not happen this boot.
|
|
1163
|
+
markAgentClisReady();
|
|
1164
|
+
}),
|
|
1143
1165
|
);
|
|
1144
1166
|
}
|
|
1145
1167
|
|
|
1168
|
+
/** Last successful pass per engine configuration (in-process). */
|
|
1169
|
+
const lastOkByKey = new Map<string, { at: number; result: StandardImageResult }>();
|
|
1170
|
+
|
|
1171
|
+
/** Tests: forget every remembered pass. */
|
|
1172
|
+
export function resetStandardImageMemo(): void {
|
|
1173
|
+
lastOkByKey.clear();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** The owned-host schedule (docs/agent-notes.md, "image maintenance"). */
|
|
1177
|
+
export const MAINTENANCE_INTERVAL_MS = 60 * 60_000;
|
|
1178
|
+
|
|
1146
1179
|
async function ensureStandardImageInner(
|
|
1147
1180
|
engines: OptionalEngineConfiguration,
|
|
1148
1181
|
): Promise<StandardImageResult> {
|
|
@@ -128,7 +128,7 @@ registry.register(appleProvider);
|
|
|
128
128
|
// host) until machine-backing becomes a per-task property.
|
|
129
129
|
// ---------------------------------------------------------------------------
|
|
130
130
|
|
|
131
|
-
function machineTasksEnabled(): boolean {
|
|
131
|
+
export function machineTasksEnabled(): boolean {
|
|
132
132
|
return process.env.UAI_MACHINE_TASKS === "1";
|
|
133
133
|
}
|
|
134
134
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
} from "../lib/container-runtime";
|
|
78
78
|
import { vnodeAdmissionProblem } from "../lib/vnode-pressure";
|
|
79
79
|
import {
|
|
80
|
+
machineTasksEnabled,
|
|
80
81
|
provisionTaskEnvironment,
|
|
81
82
|
reconstructPersistedTaskEnvironment,
|
|
82
83
|
} from "../lib/task-environment";
|
|
@@ -792,7 +793,10 @@ export const hostCommands: HostCommands = {
|
|
|
792
793
|
// a real build only when missing/stale (Docker is definitely up now, a
|
|
793
794
|
// task just arrived). On a build FAILURE, surface WHY to the cloud (not
|
|
794
795
|
// just the host log — the operator can't read a remote user's machine).
|
|
795
|
-
|
|
796
|
+
// A pass that succeeded within the hour is reused (2026-09-09: the
|
|
797
|
+
// re-check cost 16 s of every task start); the hourly schedule in
|
|
798
|
+
// main.ts and the boot pass keep it fresh.
|
|
799
|
+
const img = await ensureStandardImage({ maxAgeMs: 60 * 60_000 });
|
|
796
800
|
if (!img.ok) {
|
|
797
801
|
throw new AgentError(
|
|
798
802
|
"STANDARD_IMAGE_UNAVAILABLE",
|
|
@@ -815,6 +819,13 @@ export const hostCommands: HostCommands = {
|
|
|
815
819
|
"GitHub is connected on this host, but Uai could not prepare its credential for Git. Retry the task; if it persists, reconnect GitHub on this host.",
|
|
816
820
|
);
|
|
817
821
|
}
|
|
822
|
+
// Launch screen: the first stage boundary. A machine-backed task
|
|
823
|
+
// spends the next minute launching an EC2 instance; a container
|
|
824
|
+
// task goes straight to preparing its workspace.
|
|
825
|
+
orchestrator.emitTaskStage(
|
|
826
|
+
input.task.id,
|
|
827
|
+
machineTasksEnabled() ? "machine" : "workspace",
|
|
828
|
+
);
|
|
818
829
|
try {
|
|
819
830
|
const provisioned = gitCredential
|
|
820
831
|
? await gitCredential.run(() =>
|
package/src/main.ts
CHANGED
|
@@ -179,6 +179,7 @@ import {
|
|
|
179
179
|
import "../lib/agents/factory";
|
|
180
180
|
import {
|
|
181
181
|
areAgentClisReady,
|
|
182
|
+
MAINTENANCE_INTERVAL_MS,
|
|
182
183
|
configuredOptionalEngines,
|
|
183
184
|
ensureStandardImage,
|
|
184
185
|
onAgentClisReady,
|
|
@@ -468,6 +469,17 @@ void reconcileEngineAccountsFromStore({ hostId })
|
|
|
468
469
|
`[engine-accounts] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
469
470
|
);
|
|
470
471
|
});
|
|
472
|
+
// Image + agent-CLI maintenance runs where the compute is owned: an owned
|
|
473
|
+
// host keeps its own image current on an hourly schedule (a task-up reuses
|
|
474
|
+
// the last pass instead of blocking on one); a Uai-operated host sets
|
|
475
|
+
// UAI_IMAGE_MAINTENANCE=external — a cloud job keeps the payload AMI fresh —
|
|
476
|
+
// and reconciles only once, at start.
|
|
477
|
+
if (env.UAI_IMAGE_MAINTENANCE === "host") {
|
|
478
|
+
const maintenanceTimer = setInterval(() => {
|
|
479
|
+
void ensureStandardImage().catch(() => undefined);
|
|
480
|
+
}, MAINTENANCE_INTERVAL_MS);
|
|
481
|
+
maintenanceTimer.unref?.();
|
|
482
|
+
}
|
|
471
483
|
// ADR-135: one refresher per rotating credential. Owned host → the in-process
|
|
472
484
|
// loop refreshes and re-pushes; Uai-operated host → the broker Lambda
|
|
473
485
|
// refreshes and this host picks up changed parameter versions.
|
package/src/protocol.ts
CHANGED
|
@@ -1831,6 +1831,15 @@ export type HostToCloud =
|
|
|
1831
1831
|
| EngineConnectEventFrame
|
|
1832
1832
|
| HostConfigAckFrame;
|
|
1833
1833
|
|
|
1834
|
+
/** Task start stages, in order (docs/agent-notes.md "task start screen"). The
|
|
1835
|
+
* host emits them as `task.stage` events while a task comes up; the page
|
|
1836
|
+
* shows a launch screen instead of the chat until `ready`. */
|
|
1837
|
+
export const TASK_START_STAGES = ["machine", "workspace", "tools", "agents", "ready"] as const;
|
|
1838
|
+
export type TaskStartStage = (typeof TASK_START_STAGES)[number];
|
|
1839
|
+
export function isTaskStartStage(value: unknown): value is TaskStartStage {
|
|
1840
|
+
return (TASK_START_STAGES as readonly unknown[]).includes(value);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1834
1843
|
export type HostEvent =
|
|
1835
1844
|
| {
|
|
1836
1845
|
kind: "task.status_changed";
|
|
@@ -1838,6 +1847,7 @@ export type HostEvent =
|
|
|
1838
1847
|
from: string;
|
|
1839
1848
|
to: string;
|
|
1840
1849
|
}
|
|
1850
|
+
| { kind: "task.stage"; taskId: string; stage: TaskStartStage; at: number }
|
|
1841
1851
|
| {
|
|
1842
1852
|
kind: "task.log";
|
|
1843
1853
|
taskId: string;
|