@runuai/host 0.9.115 → 0.9.116

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 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"),
@@ -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(): Promise<StandardImageResult> {
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
- const engines = configuredOptionalEngines();
1137
- return ensureQueue.run(optionalEngineConfigurationKey(engines), () =>
1138
- ensureStandardImageInner(engines).finally(() => {
1139
- // Always signal — even a docker-down bail — so agent spawns never hang
1140
- // waiting on a reconcile that will not happen this boot.
1141
- markAgentClisReady();
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> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.115",
3
+ "version": "0.9.116",
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>",
package/src/index.ts CHANGED
@@ -792,7 +792,10 @@ export const hostCommands: HostCommands = {
792
792
  // a real build only when missing/stale (Docker is definitely up now, a
793
793
  // task just arrived). On a build FAILURE, surface WHY to the cloud (not
794
794
  // just the host log — the operator can't read a remote user's machine).
795
- const img = await ensureStandardImage();
795
+ // A pass that succeeded within the hour is reused (2026-09-09: the
796
+ // re-check cost 16 s of every task start); the hourly schedule in
797
+ // main.ts and the boot pass keep it fresh.
798
+ const img = await ensureStandardImage({ maxAgeMs: 60 * 60_000 });
796
799
  if (!img.ok) {
797
800
  throw new AgentError(
798
801
  "STANDARD_IMAGE_UNAVAILABLE",
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.