@kici-dev/agent 0.4.0 → 0.6.0

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.
Files changed (43) hide show
  1. package/dist/checkout/clone-job-repos.d.ts +53 -0
  2. package/dist/checkout/credential-helper-bin.d.ts +28 -0
  3. package/dist/checkout/credential-helper-host.d.ts +48 -0
  4. package/dist/checkout/credential-helper.d.ts +44 -0
  5. package/dist/checkout/git-clone.d.ts +26 -0
  6. package/dist/checkout/grant-table.d.ts +30 -0
  7. package/dist/checkout/job-git-credentials.d.ts +49 -0
  8. package/dist/checkout/write-elevation.d.ts +40 -0
  9. package/dist/config.d.ts +38 -0
  10. package/dist/container-ts-loader-hook.js +2047 -1814
  11. package/dist/execution/between-jobs-controller.d.ts +50 -0
  12. package/dist/execution/between-jobs-reset.d.ts +25 -0
  13. package/dist/execution/cleanup-rerun.d.ts +21 -0
  14. package/dist/execution/download.d.ts +27 -2
  15. package/dist/execution/dynamic-job-serializer.d.ts +6 -2
  16. package/dist/execution/generator-context.d.ts +54 -0
  17. package/dist/execution/global-eval-runner.d.ts +92 -0
  18. package/dist/execution/global-workflow-env.d.ts +57 -0
  19. package/dist/execution/image-build/build-engine.d.ts +75 -0
  20. package/dist/execution/image-build/build-step.d.ts +57 -0
  21. package/dist/execution/image-build/resolve-build-spec.d.ts +41 -0
  22. package/dist/execution/image-build/runtime-facts.d.ts +31 -0
  23. package/dist/execution/init-runner.d.ts +60 -3
  24. package/dist/execution/job-runner.d.ts +146 -0
  25. package/dist/execution/sandbox/bare-metal-sandbox.d.ts +25 -1
  26. package/dist/execution/sandbox/container-sandbox.d.ts +97 -2
  27. package/dist/execution/sandbox/fork-runner.d.ts +55 -1
  28. package/dist/execution/sandbox/image-preflight.d.ts +38 -0
  29. package/dist/execution/sandbox/ipc-protocol.d.ts +96 -2
  30. package/dist/execution/sandbox/kici-runtime.d.ts +48 -0
  31. package/dist/execution/sandbox/step-loop.d.ts +17 -1
  32. package/dist/execution/sandbox/types.d.ts +55 -1
  33. package/dist/execution/sandbox/workflow-runner.d.ts +75 -4
  34. package/dist/execution/workflow-loader.d.ts +8 -1
  35. package/dist/idle-shutdown.d.ts +24 -0
  36. package/dist/index.js +221 -80
  37. package/dist/metrics/prometheus.d.ts +36 -10
  38. package/dist/server.js +3197 -462
  39. package/dist/workflow-runner-bundle.js +42207 -41104
  40. package/dist/workflow-runner.js +594 -196
  41. package/dist/ws/orchestrator-client.d.ts +95 -3
  42. package/package.json +10 -10
  43. package/sbom.spdx.json +464 -454
package/dist/index.js CHANGED
@@ -6,12 +6,12 @@ import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/s
6
6
  import { KNOWN_ROLES, parseHostPropertyAssignments, validateNoReservedLabels } from "@kici-dev/engine";
7
7
  import { execFile } from "node:child_process";
8
8
  import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
9
- import { makeTempDir } from "@kici-dev/core/tmp";
9
+ import { existsSync } from "node:fs";
10
10
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
+ import { makeTempDir } from "@kici-dev/core/tmp";
11
12
  import { promisify } from "node:util";
12
13
  import { createLogger, sha256, toErrorMessage } from "@kici-dev/shared";
13
14
  import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
14
- import { existsSync } from "node:fs";
15
15
  import { parse, stringify } from "yaml";
16
16
  import { Readable, Transform } from "node:stream";
17
17
  import { pipeline } from "node:stream/promises";
@@ -40,6 +40,20 @@ var __exportAll = (all, no_symbols) => {
40
40
  return target;
41
41
  };
42
42
  //#endregion
43
+ //#region src/execution/image-build/build-engine.ts
44
+ /**
45
+ * Build a job's container image with the host's build CLI.
46
+ *
47
+ * The CLI is REQUIRED — there is deliberately no socket-API fallback. One build
48
+ * path means one set of Dockerfile semantics: `.dockerignore`, BuildKit and
49
+ * every directive behave as they do on the author's own machine, instead of
50
+ * depending on which agent happened to pick the job up. A host without a CLI
51
+ * cannot run a Dockerfile job, and says so in as many words.
52
+ */
53
+ /** The build CLIs an agent host may provide. */
54
+ const ContainerBuildCli = z.enum(["docker", "podman"]);
55
+ ContainerBuildCli.enum.docker, ContainerBuildCli.enum.podman;
56
+ //#endregion
43
57
  //#region src/config.ts
44
58
  /** Execution mode for the agent's sandbox backend. Mirrors the runtime enum. */
45
59
  const ExecutionMode = z.enum([
@@ -47,6 +61,71 @@ const ExecutionMode = z.enum([
47
61
  "bare-metal",
48
62
  "firecracker"
49
63
  ]);
64
+ /** When the operator between-jobs reset command runs. */
65
+ const BetweenJobsRunOn = z.enum(["always", "on-failure"]);
66
+ const configSchema = z.object({
67
+ orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
68
+ agentId: z.string().optional(),
69
+ labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
70
+ properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
71
+ roles: z.string().optional().transform((s) => {
72
+ if (s === void 0) return void 0;
73
+ if (s === "") return [];
74
+ return s.split(",").filter(Boolean);
75
+ }).refine((roles) => {
76
+ if (roles === void 0) return true;
77
+ const validValues = [...KNOWN_ROLES, "all"];
78
+ return roles.every((r) => validValues.includes(r));
79
+ }, { message: `KICI_ROLES must contain only: ${[...KNOWN_ROLES, "all"].join(", ")}` }).transform((roles) => {
80
+ if (roles === void 0) return void 0;
81
+ if (roles.includes("all")) return void 0;
82
+ if (roles.length === 0) return [];
83
+ return roles.filter((r) => r !== "all");
84
+ }),
85
+ port: z.coerce.number().default(8080),
86
+ logLevel: z.enum([
87
+ "debug",
88
+ "info",
89
+ "warn",
90
+ "error"
91
+ ]).default("info"),
92
+ agentToken: z.string().optional(),
93
+ githubToken: z.string().optional(),
94
+ maxLogSizeBytes: z.coerce.number().default(10485760),
95
+ defaultStepTimeoutMs: z.coerce.number().default(18e5),
96
+ dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
97
+ jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
98
+ backpressureMode: z.enum(["pause", "drop"]).default("pause"),
99
+ agentPayloadDir: z.string().optional(),
100
+ agentCommand: z.string().optional(),
101
+ sandbox: z.string().default("false").transform((s) => s === "true"),
102
+ trustedEnv: z.string().default("false").transform((s) => s === "true"),
103
+ inPlace: z.string().default("false").transform((s) => s === "true"),
104
+ sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
105
+ sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
106
+ sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
107
+ sandboxUser: z.string().optional(),
108
+ sandboxPidsLimit: z.coerce.number().int().positive().default(512),
109
+ sandboxMemoryBytes: z.coerce.number().int().positive().default(2147483648),
110
+ sandboxNanoCpus: z.coerce.number().int().positive().default(2e9),
111
+ scalerManaged: z.string().optional().transform((s) => s === "1"),
112
+ jobImageAgent: z.string().optional().transform((s) => s === "1"),
113
+ runtimeImage: z.string().optional(),
114
+ runtimeNodeSource: z.string().optional(),
115
+ containerBuildCli: ContainerBuildCli.optional(),
116
+ scalerClaimCode: z.string().optional(),
117
+ scalerIdleTimeoutMs: z.coerce.number().default(5e3),
118
+ scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
119
+ executionMode: ExecutionMode.optional(),
120
+ otelExporterOtlpEndpoint: z.string().optional(),
121
+ concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
122
+ isOrchestratorHost: z.string().optional().transform((v) => v === "true"),
123
+ betweenJobsResetCommand: z.string().optional(),
124
+ betweenJobsResetTimeoutMs: z.coerce.number().int().positive().default(6e4),
125
+ betweenJobsResetRunOn: BetweenJobsRunOn.default("always"),
126
+ orphanCleanup: z.string().default("true").transform((s) => s !== "false"),
127
+ drainOnResetFailure: z.string().default("false").transform((s) => s === "true")
128
+ });
50
129
  /**
51
130
  * Env-var definition for the agent. Exported so the docs generator and the
52
131
  * deploy-stg pre-validator can inspect / re-parse without round-tripping
@@ -54,59 +133,7 @@ const ExecutionMode = z.enum([
54
133
  */
55
134
  const envDef = defineEnv({
56
135
  service: "agent",
57
- schema: z.object({
58
- orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
59
- agentId: z.string().optional(),
60
- labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
61
- properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
62
- roles: z.string().optional().transform((s) => {
63
- if (s === void 0) return void 0;
64
- if (s === "") return [];
65
- return s.split(",").filter(Boolean);
66
- }).refine((roles) => {
67
- if (roles === void 0) return true;
68
- const validValues = [...KNOWN_ROLES, "all"];
69
- return roles.every((r) => validValues.includes(r));
70
- }, { message: `KICI_ROLES must contain only: ${[...KNOWN_ROLES, "all"].join(", ")}` }).transform((roles) => {
71
- if (roles === void 0) return void 0;
72
- if (roles.includes("all")) return void 0;
73
- if (roles.length === 0) return [];
74
- return roles.filter((r) => r !== "all");
75
- }),
76
- port: z.coerce.number().default(8080),
77
- logLevel: z.enum([
78
- "debug",
79
- "info",
80
- "warn",
81
- "error"
82
- ]).default("info"),
83
- agentToken: z.string().optional(),
84
- githubToken: z.string().optional(),
85
- maxLogSizeBytes: z.coerce.number().default(10 * 1024 * 1024),
86
- defaultStepTimeoutMs: z.coerce.number().default(1800 * 1e3),
87
- dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
88
- jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
89
- backpressureMode: z.enum(["pause", "drop"]).default("pause"),
90
- agentPayloadDir: z.string().optional(),
91
- agentCommand: z.string().optional(),
92
- sandbox: z.string().default("false").transform((s) => s === "true"),
93
- trustedEnv: z.string().default("false").transform((s) => s === "true"),
94
- inPlace: z.string().default("false").transform((s) => s === "true"),
95
- sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
96
- sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
97
- sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
98
- sandboxUser: z.string().optional(),
99
- sandboxPidsLimit: z.coerce.number().int().positive().default(512),
100
- sandboxMemoryBytes: z.coerce.number().int().positive().default(2 * 1024 * 1024 * 1024),
101
- sandboxNanoCpus: z.coerce.number().int().positive().default(2 * 1e9),
102
- scalerManaged: z.string().optional().transform((s) => s === "1"),
103
- scalerIdleTimeoutMs: z.coerce.number().default(5e3),
104
- scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
105
- executionMode: ExecutionMode.optional(),
106
- otelExporterOtlpEndpoint: z.string().optional(),
107
- concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
108
- isOrchestratorHost: z.string().optional().transform((v) => v === "true")
109
- }),
136
+ schema: configSchema,
110
137
  envMap: {
111
138
  orchestratorUrl: "KICI_ORCHESTRATOR_URL",
112
139
  agentId: "KICI_AGENT_ID",
@@ -136,11 +163,21 @@ const envDef = defineEnv({
136
163
  sandboxMemoryBytes: "KICI_SANDBOX_MEMORY_BYTES",
137
164
  sandboxNanoCpus: "KICI_SANDBOX_NANO_CPUS",
138
165
  scalerManaged: "KICI_SCALER_MANAGED",
166
+ jobImageAgent: "KICI_JOB_IMAGE_AGENT",
167
+ runtimeImage: "KICI_RUNTIME_IMAGE",
168
+ runtimeNodeSource: "KICI_RUNTIME_NODE_SOURCE",
169
+ containerBuildCli: "KICI_CONTAINER_BUILD_CLI",
170
+ scalerClaimCode: "KICI_SCALER_CLAIM_CODE",
139
171
  scalerIdleTimeoutMs: "KICI_SCALER_IDLE_TIMEOUT",
140
172
  scalerPendingDispatchTimeoutMs: "KICI_SCALER_PENDING_DISPATCH_TIMEOUT",
141
173
  executionMode: "KICI_EXECUTION_MODE",
142
174
  otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
143
- concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS"
175
+ concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS",
176
+ betweenJobsResetCommand: "KICI_AGENT_BETWEEN_JOBS_RESET_COMMAND",
177
+ betweenJobsResetTimeoutMs: "KICI_AGENT_BETWEEN_JOBS_RESET_TIMEOUT_MS",
178
+ betweenJobsResetRunOn: "KICI_AGENT_BETWEEN_JOBS_RESET_RUN_ON",
179
+ orphanCleanup: "KICI_AGENT_ORPHAN_CLEANUP",
180
+ drainOnResetFailure: "KICI_AGENT_DRAIN_ON_RESET_FAILURE"
144
181
  }
145
182
  });
146
183
  /**
@@ -172,10 +209,16 @@ const envDef = defineEnv({
172
209
  * - KICI_SANDBOX_MEMORY_BYTES (default: 2 GiB) — memory cap in bytes for the job container cgroup
173
210
  * - KICI_SANDBOX_NANO_CPUS (default: 2 CPUs) — CPU cap in nano-CPUs for the job container cgroup
174
211
  * - KICI_SCALER_MANAGED (set to "1" by the orchestrator's auto-scaler — agent self-shuts down on idle)
212
+ * - KICI_SCALER_CLAIM_CODE (optional single-use claim code the agent exchanges for its own ephemeral credentials before registering; ignored when KICI_AGENT_TOKEN is set)
175
213
  * - KICI_SCALER_IDLE_TIMEOUT (ms, default 5000) — how long a scaler-managed agent waits before shutdown after going idle
176
214
  * - KICI_SCALER_PENDING_DISPATCH_TIMEOUT (ms, default 60000) — extended idle window when register.ack signals a queued bound job
177
215
  * - KICI_EXECUTION_MODE (optional, options: container | bare-metal | firecracker) — override the runner's mode-pick logic
178
216
  * - KICI_CONCURRENCY_WAIT_TIMEOUT_MS (default: 3_600_000) — workflow-runner timeout when long-polling for a slot-release follow-up `concurrency.ack`
217
+ * - KICI_AGENT_BETWEEN_JOBS_RESET_COMMAND (optional) — host-reset command run between jobs on a reused agent (fail-open)
218
+ * - KICI_AGENT_BETWEEN_JOBS_RESET_TIMEOUT_MS (default: 60000) — reset command timeout
219
+ * - KICI_AGENT_BETWEEN_JOBS_RESET_RUN_ON (default: always, options: always | on-failure) — when the reset command runs
220
+ * - KICI_AGENT_ORPHAN_CLEANUP (default: true) — reap a finished job's leaked process tree (bare-metal); false = only signal the runner child
221
+ * - KICI_AGENT_DRAIN_ON_RESET_FAILURE (default: false) — drain the agent after repeated consecutive reset failures
179
222
  */
180
223
  function loadConfig() {
181
224
  const data = envDef.parse();
@@ -748,11 +791,11 @@ function isAbsoluteRel(rel) {
748
791
  * env vars — the install runs with `--ignore-scripts` whenever a private
749
792
  * registry is configured.
750
793
  */
751
- const logger$2 = createLogger({ prefix: "dep-installer" });
794
+ const logger$3 = createLogger({ prefix: "dep-installer" });
752
795
  const execFileAsync = promisify(execFile);
753
796
  /** Install subprocess timeout (10 min) and stdout/stderr buffer (128 MiB). */
754
797
  const INSTALL_TIMEOUT_MS = 6e5;
755
- const INSTALL_MAX_BUFFER = 128 * 1024 * 1024;
798
+ const INSTALL_MAX_BUFFER = 134217728;
756
799
  /**
757
800
  * Detect the package manager for the cloned repo from its committed manifests.
758
801
  * A pnpm workspace's `packageManager` field + `pnpm-lock.yaml` live at the repo
@@ -793,7 +836,7 @@ async function installDeps(kiciDir, opts = {}) {
793
836
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
794
837
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
795
838
  const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
796
- logger$2.info("Installing deps inline", {
839
+ logger$3.info("Installing deps inline", {
797
840
  packageManager,
798
841
  yarnFlavor,
799
842
  dir: kiciDir
@@ -851,7 +894,7 @@ async function installDeps(kiciDir, opts = {}) {
851
894
  if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
852
895
  const durationMs = Date.now() - startTime;
853
896
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
854
- logger$2.info("Deps installed inline", {
897
+ logger$3.info("Deps installed inline", {
855
898
  packageManager,
856
899
  durationMs
857
900
  });
@@ -1143,10 +1186,25 @@ var init_dep_restore = __esmMin((() => {
1143
1186
  * dep-restore.ts and workflow-loader.ts.
1144
1187
  */
1145
1188
  var download_exports = /* @__PURE__ */ __exportAll({
1189
+ UPLOAD_MAX_RETRIES: () => 2,
1146
1190
  downloadUrl: () => downloadUrl,
1147
1191
  uploadToPresignedUrl: () => uploadToPresignedUrl
1148
1192
  });
1149
1193
  /**
1194
+ * Whether a failed upload attempt is worth repeating.
1195
+ *
1196
+ * A transport failure (connection refused, reset, DNS) never reached a
1197
+ * responder, and 5xx / 429 are the object-storage overload signals AWS
1198
+ * documents as retry-with-backoff (S3 answers `SlowDown` with 503). Every other
1199
+ * status is a decision the server will repeat: a 403 from an expired or
1200
+ * malformed signature, a 400 from a malformed request. Retrying those burns the
1201
+ * ceiling without a chance of success and delays the real error.
1202
+ */
1203
+ function isRetryableUploadFailure(err) {
1204
+ if (!(err instanceof PresignedUploadHttpError)) return true;
1205
+ return err.statusCode >= 500 || err.statusCode === 429;
1206
+ }
1207
+ /**
1150
1208
  * Download content from an HTTP/HTTPS URL.
1151
1209
  *
1152
1210
  * Includes a 5-minute timeout to prevent the agent from hanging indefinitely
@@ -1170,21 +1228,10 @@ function downloadUrl(url) {
1170
1228
  }).on("error", reject);
1171
1229
  });
1172
1230
  }
1173
- /**
1174
- * Upload a buffer to a pre-signed S3 URL via HTTP PUT.
1175
- *
1176
- * Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
1177
- * 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
1178
- * filesystem cache backend's signed URLs work from container agents that
1179
- * can't reach the orchestrator's host loopback directly.
1180
- *
1181
- * @param url - The pre-signed URL to upload to
1182
- * @param data - The buffer to upload
1183
- */
1184
- function uploadToPresignedUrl(url, data) {
1231
+ /** One PUT of the whole buffer. Rejects with {@link PresignedUploadHttpError} on a non-2xx. */
1232
+ function putOnce(resolvedUrl, data, timeoutMs) {
1185
1233
  return new Promise((resolve, reject) => {
1186
- const resolved = resolveOrchestratorUrl(url);
1187
- const parsed = new URL(resolved);
1234
+ const parsed = new URL(resolvedUrl);
1188
1235
  const req = (parsed.protocol === "https:" ? https : http).request({
1189
1236
  hostname: parsed.hostname,
1190
1237
  port: parsed.port,
@@ -1193,7 +1240,7 @@ function uploadToPresignedUrl(url, data) {
1193
1240
  headers: { "Content-Length": data.length }
1194
1241
  }, (res) => {
1195
1242
  if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
1196
- reject(/* @__PURE__ */ new Error(`HTTP ${res.statusCode} uploading to pre-signed URL`));
1243
+ reject(new PresignedUploadHttpError(res.statusCode));
1197
1244
  res.resume();
1198
1245
  return;
1199
1246
  }
@@ -1201,14 +1248,80 @@ function uploadToPresignedUrl(url, data) {
1201
1248
  res.on("end", () => resolve());
1202
1249
  res.on("error", reject);
1203
1250
  });
1251
+ req.setTimeout(timeoutMs, () => {
1252
+ req.destroy(/* @__PURE__ */ new Error(`Pre-signed upload timed out after ${timeoutMs}ms`));
1253
+ });
1204
1254
  req.on("error", reject);
1205
1255
  req.end(data);
1206
1256
  });
1207
1257
  }
1208
- var DOWNLOAD_TIMEOUT_MS$1;
1258
+ /**
1259
+ * Upload a buffer to a pre-signed S3 URL via HTTP PUT, retrying a transient
1260
+ * failure.
1261
+ *
1262
+ * Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
1263
+ * 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
1264
+ * filesystem cache backend's signed URLs work from container agents that
1265
+ * can't reach the orchestrator's host loopback directly.
1266
+ *
1267
+ * **Why retrying is safe here.** A pre-signed PUT writes one whole object at a
1268
+ * single key: there is no multipart session, no append, and no
1269
+ * server-generated identity, so a repeat attempt writes the same bytes to the
1270
+ * same key and the last write wins. S3 also only makes an object visible once
1271
+ * the body has been received in full, so an attempt that died mid-body left
1272
+ * nothing behind. A retry therefore cannot double-write or produce a torn
1273
+ * object — which is why every AWS SDK retries PUTs by default.
1274
+ *
1275
+ * Only a failure that can plausibly differ next time is repeated — see
1276
+ * {@link isRetryableUploadFailure}.
1277
+ *
1278
+ * @param url - The pre-signed URL to upload to
1279
+ * @param data - The buffer to upload
1280
+ * @param opts.baseDelayMs - Backoff before the first retry (doubles thereafter)
1281
+ * @param opts.timeoutMs - Per-attempt socket-inactivity timeout (see
1282
+ * {@link UPLOAD_TIMEOUT_MS}); an override exists so a test can drive the
1283
+ * stall path without waiting out the production budget.
1284
+ */
1285
+ async function uploadToPresignedUrl(url, data, opts) {
1286
+ const resolved = resolveOrchestratorUrl(url);
1287
+ const baseDelayMs = opts?.baseDelayMs ?? UPLOAD_RETRY_BASE_DELAY_MS;
1288
+ const timeoutMs = opts?.timeoutMs ?? UPLOAD_TIMEOUT_MS;
1289
+ let lastError;
1290
+ for (let attempt = 0; attempt <= 2; attempt++) {
1291
+ if (attempt > 0) {
1292
+ const delayMs = baseDelayMs * 2 ** (attempt - 1);
1293
+ logger$1.warn("Retrying pre-signed upload", {
1294
+ attempt,
1295
+ delayMs,
1296
+ error: lastError?.message
1297
+ });
1298
+ await new Promise((r) => setTimeout(r, delayMs));
1299
+ }
1300
+ try {
1301
+ await putOnce(resolved, data, timeoutMs);
1302
+ return;
1303
+ } catch (err) {
1304
+ lastError = err instanceof Error ? err : new Error(String(err));
1305
+ if (!isRetryableUploadFailure(lastError)) throw lastError;
1306
+ }
1307
+ }
1308
+ throw new Error(`Pre-signed upload failed after 3 attempts: ${lastError?.message}`);
1309
+ }
1310
+ var logger$1, DOWNLOAD_TIMEOUT_MS$1, UPLOAD_TIMEOUT_MS, UPLOAD_RETRY_BASE_DELAY_MS, PresignedUploadHttpError;
1209
1311
  var init_download = __esmMin((() => {
1210
1312
  init_dep_restore();
1211
- DOWNLOAD_TIMEOUT_MS$1 = 300 * 1e3;
1313
+ logger$1 = createLogger({ prefix: "agent:download" });
1314
+ DOWNLOAD_TIMEOUT_MS$1 = 3e5;
1315
+ UPLOAD_TIMEOUT_MS = 3e5;
1316
+ UPLOAD_RETRY_BASE_DELAY_MS = 500;
1317
+ PresignedUploadHttpError = class extends Error {
1318
+ statusCode;
1319
+ constructor(statusCode) {
1320
+ super(`HTTP ${statusCode} uploading to pre-signed URL`);
1321
+ this.statusCode = statusCode;
1322
+ this.name = "PresignedUploadHttpError";
1323
+ }
1324
+ };
1212
1325
  }));
1213
1326
  //#endregion
1214
1327
  //#region src/execution/cache/cache-engine.ts
@@ -1231,11 +1344,13 @@ var init_download = __esmMin((() => {
1231
1344
  * right destination (repo entries under `workDir`, home entries under the
1232
1345
  * homedir). Extraction lands in a scratch dir first, then moves each group
1233
1346
  * into place so a partial restore never leaves half-written paths in the live
1234
- * tree (mirrors dep-restore).
1347
+ * tree (mirrors dep-restore). The scratch dir honors `KICI_TMPDIR`, which may
1348
+ * sit on a different filesystem from the workspace, so the move falls back to
1349
+ * copy-then-remove on `EXDEV` rather than assuming a same-filesystem rename.
1235
1350
  */
1236
1351
  const logger = createLogger({ prefix: "cache-engine" });
1237
1352
  /** Download timeout for a presigned cache GET: 5 minutes. */
1238
- const DOWNLOAD_TIMEOUT_MS = 300 * 1e3;
1353
+ const DOWNLOAD_TIMEOUT_MS = 3e5;
1239
1354
  /**
1240
1355
  * Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
1241
1356
  * Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
@@ -1310,6 +1425,32 @@ async function packCachePaths(workDir, paths, roots) {
1310
1425
  await cleanup();
1311
1426
  }
1312
1427
  }
1428
+ /**
1429
+ * Move a tree from `src` to `dest`. Attempts a rename (same-filesystem, cheap)
1430
+ * and falls back to copy-then-remove only on `EXDEV` — the errno `rename(2)`
1431
+ * reports when the scratch dir (which honors `KICI_TMPDIR`) is on a different
1432
+ * filesystem from the destination workspace. `verbatimSymlinks` keeps a cached
1433
+ * symlink graph intact (mirroring packCachePaths), and `preserveTimestamps`
1434
+ * keeps mtimes stable so the fallback is byte-for-byte equivalent to the
1435
+ * rename. Any non-`EXDEV` error propagates so a real permission or corruption
1436
+ * failure is never silently turned into a copy.
1437
+ */
1438
+ async function moveOrCopy(src, dest) {
1439
+ try {
1440
+ await rename(src, dest);
1441
+ } catch (err) {
1442
+ if (err.code !== "EXDEV") throw err;
1443
+ await cp(src, dest, {
1444
+ recursive: true,
1445
+ verbatimSymlinks: true,
1446
+ preserveTimestamps: true
1447
+ });
1448
+ await rm(src, {
1449
+ recursive: true,
1450
+ force: true
1451
+ });
1452
+ }
1453
+ }
1313
1454
  /** Move the extracted `__repo__` / `__home__` groups from a scratch dir into place. */
1314
1455
  async function moveAnchoredGroups(scratchDir, workDir, home) {
1315
1456
  for (const anchor of await readdir(scratchDir)) {
@@ -1323,7 +1464,7 @@ async function moveAnchoredGroups(scratchDir, workDir, home) {
1323
1464
  recursive: true,
1324
1465
  force: true
1325
1466
  });
1326
- await rename(join(anchorDir, child), dest);
1467
+ await moveOrCopy(join(anchorDir, child), dest);
1327
1468
  }
1328
1469
  }
1329
1470
  }
@@ -2,40 +2,40 @@
2
2
  * Total completed jobs.
3
3
  * Labels:
4
4
  * - status: success | failed | cancelled
5
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
5
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
6
6
  */
7
7
  export declare const jobsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
8
8
  /**
9
9
  * Currently running jobs.
10
10
  * Labels:
11
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
11
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
12
12
  */
13
13
  export declare const jobsActive: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
14
14
  /**
15
15
  * Total completed steps.
16
16
  * Labels:
17
17
  * - status: success | failed | skipped
18
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
18
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
19
19
  */
20
20
  export declare const stepsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
21
21
  /**
22
22
  * Step execution duration in seconds.
23
23
  * Advisory boundaries cover sub-second steps through 30-minute long-running steps.
24
24
  * Labels:
25
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
25
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
26
26
  */
27
27
  export declare const stepDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
28
28
  /**
29
29
  * Git clone duration in seconds.
30
30
  * Advisory boundaries cover fast shallow clones through large repo clones.
31
31
  * Labels:
32
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
32
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
33
33
  */
34
34
  export declare const cloneDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
35
35
  /**
36
36
  * Total log bytes streamed back to orchestrator.
37
37
  * Labels:
38
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
38
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
39
39
  */
40
40
  export declare const logBytesTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
41
41
  /**
@@ -51,7 +51,7 @@ export declare const logBytesTotal: import("@opentelemetry/api").Counter<import(
51
51
  * Labels:
52
52
  * - mode: `pause` (producer paused, no data loss) | `drop` (lines
53
53
  * discarded with a `[N lines dropped due to backpressure]` marker)
54
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
54
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
55
55
  */
56
56
  export declare const logBackpressureEventsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
57
57
  /**
@@ -64,7 +64,7 @@ export declare const logBackpressureEventsTotal: import("@opentelemetry/api").Co
64
64
  * producer.
65
65
  *
66
66
  * Labels:
67
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
67
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
68
68
  */
69
69
  export declare const logLinesDroppedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
70
70
  /**
@@ -81,7 +81,7 @@ export declare const logLinesDroppedTotal: import("@opentelemetry/api").Counter<
81
81
  *
82
82
  * Labels:
83
83
  * - mode: `pause` | `drop`
84
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
84
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
85
85
  */
86
86
  export declare const logBackpressureActive: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
87
87
  /**
@@ -89,7 +89,33 @@ export declare const logBackpressureActive: import("@opentelemetry/api").UpDownC
89
89
  * Use add(1) for connected, add(-1) for disconnected.
90
90
  *
91
91
  * Labels:
92
- * - scaler: injected by orch-side AgentMetricsAggregator (Phase 5b); `stateful` for static agents, backend name (`container` / `firecracker` / `bare-metal`) for scaler-managed
92
+ * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
93
93
  */
94
94
  export declare const connectionStatus: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
95
+ /**
96
+ * Total operator between-jobs reset command invocations.
97
+ * Labels:
98
+ * - status: success | failed | timeout
99
+ * - scaler: injected orch-side; `stateful` for static agents
100
+ */
101
+ export declare const betweenJobsResetTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
102
+ /**
103
+ * Between-jobs reset command duration in seconds.
104
+ * Labels:
105
+ * - scaler: injected orch-side
106
+ */
107
+ export declare const betweenJobsResetDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
108
+ /**
109
+ * Processes killed by the between-jobs process-group reap.
110
+ * Labels:
111
+ * - scaler: injected orch-side
112
+ */
113
+ export declare const orphansReapedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
114
+ /**
115
+ * Out-of-band declared-cleanup re-runs after a hard-killed runner.
116
+ * Labels:
117
+ * - status: success | failed | timeout | skipped
118
+ * - scaler: injected orch-side
119
+ */
120
+ export declare const orphanCleanupTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
95
121
  //# sourceMappingURL=prometheus.d.ts.map