@kici-dev/agent 0.1.13 → 0.1.15

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/dist/server.js CHANGED
@@ -8,16 +8,17 @@ import { PassThrough, Readable, Transform, Writable } from "node:stream";
8
8
  import { serve } from "@hono/node-server";
9
9
  import { Hono } from "hono";
10
10
  import winston from "winston";
11
- import { RingBuffer, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, deriveSharedSecret, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, normalizeLineEndings, requestContext, setServiceName, setupGracefulShutdown, sha256, sha256File, toErrorMessage, validateRequiredTools } from "@kici-dev/shared";
11
+ import { RingBuffer, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, deriveSharedSecret, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, logger, normalizeLineEndings, requestContext, setServiceName, setupGracefulShutdown, sha256, sha256File, toErrorMessage, validateRequiredTools } from "@kici-dev/shared";
12
12
  import { z } from "zod";
13
13
  import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
14
- import { ALLOWED_SYSTEM_VARS, ExecutionJobStatus, ExecutionStepStatus, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, deriveOsArchLabels, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
14
+ import { ALLOWED_SYSTEM_VARS, CacheRunEventType, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, InitFailureCategory, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, deriveOsArchLabels, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
15
15
  import { execFile, execFileSync, execSync, fork, spawn } from "node:child_process";
16
16
  import WebSocket from "ws";
17
17
  import { AsyncLocalStorage } from "node:async_hooks";
18
18
  import { format, promisify } from "node:util";
19
19
  import { existsSync } from "node:fs";
20
20
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
+ import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
21
22
  import { fileURLToPath, pathToFileURL } from "node:url";
22
23
  import fs, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
23
24
  import Docker from "dockerode";
@@ -27,7 +28,7 @@ import https from "node:https";
27
28
  import http from "node:http";
28
29
  import { pipeline } from "node:stream/promises";
29
30
  import { createGunzip } from "node:zlib";
30
- import { PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
31
+ import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
31
32
  import { createInterface } from "node:readline";
32
33
  var __defProp = Object.defineProperty;
33
34
  var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -207,7 +208,7 @@ var LogBuffer = class extends RingBuffer {
207
208
  };
208
209
  //#endregion
209
210
  //#region src/ws/orchestrator-client.ts
210
- const logger$10 = createLogger({ prefix: "orchestrator-client" });
211
+ const logger$11 = createLogger({ prefix: "orchestrator-client" });
211
212
  /**
212
213
  * WebSocket client that connects the agent to the customer orchestrator.
213
214
  *
@@ -240,6 +241,8 @@ var OrchestratorClient = class OrchestratorClient {
240
241
  pendingEventEmitRequests = /* @__PURE__ */ new Map();
241
242
  /** Pending agent.api.request calls awaiting orchestrator response. */
242
243
  pendingApiRequests = /* @__PURE__ */ new Map();
244
+ /** Pending user-cache restore/save requests awaiting orchestrator response. */
245
+ pendingUserCacheRequests = /* @__PURE__ */ new Map();
243
246
  /** Pending concurrency report requests awaiting orchestrator ack. */
244
247
  pendingConcurrencyRequests = /* @__PURE__ */ new Map();
245
248
  url;
@@ -311,7 +314,7 @@ var OrchestratorClient = class OrchestratorClient {
311
314
  */
312
315
  connect() {
313
316
  if (this._state !== "disconnected") {
314
- logger$10.warn("connect() called while not disconnected", { state: this._state });
317
+ logger$11.warn("connect() called while not disconnected", { state: this._state });
315
318
  return;
316
319
  }
317
320
  this.intentionalDisconnect = false;
@@ -481,6 +484,69 @@ var OrchestratorClient = class OrchestratorClient {
481
484
  });
482
485
  }
483
486
  /**
487
+ * Relay a user-facing cache request from the sandbox to the orchestrator.
488
+ *
489
+ * Translates the sandbox `cache.request` IPC into the matching `cache.user.*`
490
+ * WS message and resolves with the orchestrator's response mapped onto the
491
+ * IPC response shape:
492
+ *
493
+ * - `restore` -> `cache.user.restore.request`, awaits `cache.user.restore.response`.
494
+ * - `beginSave` -> `cache.user.save.request`, awaits `cache.user.save.response`.
495
+ * - `completeSave` -> `cache.user.save.complete` (fire-and-forget; the
496
+ * orchestrator commits temp -> final without replying), resolves immediately.
497
+ *
498
+ * Times out after 30 seconds for the round-trip ops.
499
+ */
500
+ async requestUserCache(jobId, request) {
501
+ if (request.op === "completeSave") {
502
+ this.sendDirect({
503
+ type: "cache.user.save.complete",
504
+ messageId: randomUUID(),
505
+ jobId,
506
+ key: request.key,
507
+ tarHash: request.tarHash,
508
+ sizeBytes: request.sizeBytes
509
+ });
510
+ return {
511
+ type: "cache.response",
512
+ requestId: request.requestId
513
+ };
514
+ }
515
+ const messageId = randomUUID();
516
+ return new Promise((resolve, reject) => {
517
+ const timer = setTimeout(() => {
518
+ this.pendingUserCacheRequests.delete(messageId);
519
+ reject(/* @__PURE__ */ new Error("User-cache request timed out (30s)"));
520
+ }, 3e4);
521
+ this.pendingUserCacheRequests.set(messageId, {
522
+ resolve: (response) => {
523
+ clearTimeout(timer);
524
+ resolve({
525
+ ...response,
526
+ requestId: request.requestId
527
+ });
528
+ },
529
+ reject: (err) => {
530
+ clearTimeout(timer);
531
+ reject(err);
532
+ }
533
+ });
534
+ if (request.op === "restore") this.sendDirect({
535
+ type: "cache.user.restore.request",
536
+ messageId,
537
+ jobId,
538
+ key: request.key,
539
+ ...request.restoreKeys && { restoreKeys: request.restoreKeys }
540
+ });
541
+ else this.sendDirect({
542
+ type: "cache.user.save.request",
543
+ messageId,
544
+ jobId,
545
+ key: request.key
546
+ });
547
+ });
548
+ }
549
+ /**
484
550
  * Send a job.context message to the orchestrator.
485
551
  *
486
552
  * Conveys execution environment details (runtime, sandbox type, env vars)
@@ -562,7 +628,7 @@ var OrchestratorClient = class OrchestratorClient {
562
628
  }
563
629
  });
564
630
  } catch (err) {
565
- logger$10.error("Failed to create WebSocket", { error: toErrorMessage(err) });
631
+ logger$11.error("Failed to create WebSocket", { error: toErrorMessage(err) });
566
632
  this._state = "disconnected";
567
633
  this.scheduleReconnect();
568
634
  return;
@@ -570,7 +636,7 @@ var OrchestratorClient = class OrchestratorClient {
570
636
  this.ws.on("open", () => {
571
637
  if (this.token) {
572
638
  this._state = "authenticating";
573
- logger$10.info("Connected to orchestrator, sending auth.request", {
639
+ logger$11.info("Connected to orchestrator, sending auth.request", {
574
640
  url: this.url,
575
641
  agentId: this.agentId
576
642
  });
@@ -581,7 +647,7 @@ var OrchestratorClient = class OrchestratorClient {
581
647
  }));
582
648
  } else {
583
649
  this._state = "registering";
584
- logger$10.info("Connected to orchestrator, sending agent.register (no token)", {
650
+ logger$11.info("Connected to orchestrator, sending agent.register (no token)", {
585
651
  url: this.url,
586
652
  agentId: this.agentId
587
653
  });
@@ -592,12 +658,12 @@ var OrchestratorClient = class OrchestratorClient {
592
658
  this.handleMessage(data);
593
659
  });
594
660
  this.ws.on("close", (code, reason) => {
595
- logger$10.info("Orchestrator connection closed", {
661
+ logger$11.info("Orchestrator connection closed", {
596
662
  code,
597
663
  reason: reason.toString()
598
664
  });
599
665
  if (code === WS_CLOSE_AGENT_AUTH_FAILED) {
600
- logger$10.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
666
+ logger$11.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
601
667
  code,
602
668
  reason: reason.toString()
603
669
  });
@@ -614,12 +680,14 @@ var OrchestratorClient = class OrchestratorClient {
614
680
  this.pendingEventEmitRequests.clear();
615
681
  for (const [_id, pending] of this.pendingApiRequests) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
616
682
  this.pendingApiRequests.clear();
683
+ for (const [_id, pending] of this.pendingUserCacheRequests) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
684
+ this.pendingUserCacheRequests.clear();
617
685
  for (const [_id, pending] of this.pendingConcurrencyRequests) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
618
686
  this.pendingConcurrencyRequests.clear();
619
687
  if (!this.intentionalDisconnect) this.scheduleReconnect();
620
688
  });
621
689
  this.ws.on("error", (err) => {
622
- logger$10.error(`Orchestrator WebSocket error: ${err.message}`);
690
+ logger$11.error(`Orchestrator WebSocket error: ${err.message}`);
623
691
  if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close();
624
692
  });
625
693
  }
@@ -628,7 +696,7 @@ var OrchestratorClient = class OrchestratorClient {
628
696
  try {
629
697
  raw = JSON.parse(data.toString());
630
698
  } catch {
631
- logger$10.warn("Malformed JSON received from orchestrator");
699
+ logger$11.warn("Malformed JSON received from orchestrator");
632
700
  return;
633
701
  }
634
702
  const rawMsg = raw;
@@ -662,19 +730,37 @@ var OrchestratorClient = class OrchestratorClient {
662
730
  }
663
731
  return;
664
732
  }
733
+ if (rawMsg.type === "cache.user.restore.response" || rawMsg.type === "cache.user.save.response") {
734
+ const cacheMsg = raw;
735
+ const pending = this.pendingUserCacheRequests.get(cacheMsg.requestId);
736
+ if (pending) {
737
+ this.pendingUserCacheRequests.delete(cacheMsg.requestId);
738
+ pending.resolve({
739
+ type: "cache.response",
740
+ requestId: cacheMsg.requestId,
741
+ ...cacheMsg.hit !== void 0 && { hit: cacheMsg.hit },
742
+ ...cacheMsg.matchedKey && { matchedKey: cacheMsg.matchedKey },
743
+ ...cacheMsg.downloadUrl && { downloadUrl: cacheMsg.downloadUrl },
744
+ ...cacheMsg.tarHash && { tarHash: cacheMsg.tarHash },
745
+ ...cacheMsg.skip !== void 0 && { skip: cacheMsg.skip },
746
+ ...cacheMsg.uploadUrl && { uploadUrl: cacheMsg.uploadUrl }
747
+ });
748
+ }
749
+ return;
750
+ }
665
751
  const parsed = orchestratorToAgentMessageSchema.safeParse(raw);
666
752
  if (parsed.success) {
667
753
  const msg = parsed.data;
668
754
  switch (msg.type) {
669
755
  case "auth.success":
670
756
  if (this._state === "authenticating") {
671
- logger$10.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
757
+ logger$11.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
672
758
  this._state = "registering";
673
759
  this.sendAgentRegister();
674
760
  }
675
761
  break;
676
762
  case "auth.failure":
677
- logger$10.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
763
+ logger$11.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
678
764
  this.authFailed = true;
679
765
  this.intentionalDisconnect = true;
680
766
  if (this.ws) {
@@ -684,7 +770,7 @@ var OrchestratorClient = class OrchestratorClient {
684
770
  this._state = "disconnected";
685
771
  break;
686
772
  case "register.ack":
687
- logger$10.info("Registration acknowledged by orchestrator", {
773
+ logger$11.info("Registration acknowledged by orchestrator", {
688
774
  agentId: msg.agentId,
689
775
  labels: msg.labels,
690
776
  scalerManaged: msg.scalerManaged,
@@ -699,14 +785,14 @@ var OrchestratorClient = class OrchestratorClient {
699
785
  this.sendConfigAck(msg.agentId);
700
786
  break;
701
787
  case "job.dispatch":
702
- logger$10.info("Job dispatch received", {
788
+ logger$11.info("Job dispatch received", {
703
789
  runId: msg.runId,
704
790
  jobId: msg.jobId
705
791
  });
706
792
  this.onJobDispatch(msg);
707
793
  break;
708
794
  case "job.cancel":
709
- logger$10.info("Job cancel received", {
795
+ logger$11.info("Job cancel received", {
710
796
  runId: msg.runId,
711
797
  jobId: msg.jobId,
712
798
  reason: msg.reason
@@ -714,7 +800,7 @@ var OrchestratorClient = class OrchestratorClient {
714
800
  this.onJobCancel(msg);
715
801
  break;
716
802
  case "job.concurrency.ack": {
717
- logger$10.info("Concurrency ack received", {
803
+ logger$11.info("Concurrency ack received", {
718
804
  requestId: msg.requestId,
719
805
  action: msg.action
720
806
  });
@@ -732,7 +818,7 @@ var OrchestratorClient = class OrchestratorClient {
732
818
  return;
733
819
  }
734
820
  if (heartbeatSchema.safeParse(raw).success) return;
735
- logger$10.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
821
+ logger$11.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
736
822
  }
737
823
  flushBuffer() {
738
824
  const events = this.eventBuffer.flush();
@@ -746,11 +832,11 @@ var OrchestratorClient = class OrchestratorClient {
746
832
  }
747
833
  this.disconnectedAt = null;
748
834
  if (events.length > 0) {
749
- logger$10.info("Flushing event buffer", { count: events.length });
835
+ logger$11.info("Flushing event buffer", { count: events.length });
750
836
  for (const msg of events) this.sendDirect(msg);
751
837
  }
752
838
  if (logLines.length > 0) {
753
- logger$10.info("Flushing log buffer", { count: logLines.length });
839
+ logger$11.info("Flushing log buffer", { count: logLines.length });
754
840
  for (let i = 0; i < logLines.length; i += OrchestratorClient.LOG_BATCH_SIZE) {
755
841
  const batch = logLines.slice(i, i + OrchestratorClient.LOG_BATCH_SIZE);
756
842
  this.sendAgentLogMessage(batch);
@@ -803,14 +889,14 @@ var OrchestratorClient = class OrchestratorClient {
803
889
  */
804
890
  blockMmdsAccess() {
805
891
  if (process.getuid?.() !== 0) {
806
- logger$10.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
892
+ logger$11.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
807
893
  return;
808
894
  }
809
895
  try {
810
896
  execSync("iptables -A OUTPUT -d 169.254.169.254 -j DROP", { timeout: 5e3 });
811
- logger$10.info("MMDS access blocked via iptables");
897
+ logger$11.info("MMDS access blocked via iptables");
812
898
  } catch (err) {
813
- logger$10.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
899
+ logger$11.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
814
900
  }
815
901
  }
816
902
  /**
@@ -824,7 +910,7 @@ var OrchestratorClient = class OrchestratorClient {
824
910
  messageId: `config-ack-${agentId}-${Date.now()}`,
825
911
  agentId
826
912
  }));
827
- logger$10.info("Config ACK sent to orchestrator", { agentId });
913
+ logger$11.info("Config ACK sent to orchestrator", { agentId });
828
914
  }
829
915
  }
830
916
  startHeartbeat() {
@@ -885,12 +971,12 @@ var OrchestratorClient = class OrchestratorClient {
885
971
  scheduleReconnect() {
886
972
  this.cancelReconnect();
887
973
  if (this.authFailed) {
888
- logger$10.error("Not reconnecting: authentication permanently failed");
974
+ logger$11.error("Not reconnecting: authentication permanently failed");
889
975
  return;
890
976
  }
891
977
  const delay = this.getReconnectDelay();
892
978
  this.reconnectAttempts++;
893
- logger$10.info("Scheduling reconnect", {
979
+ logger$11.info("Scheduling reconnect", {
894
980
  attempt: this.reconnectAttempts,
895
981
  delayMs: Math.round(delay)
896
982
  });
@@ -975,14 +1061,14 @@ var init_console_capture = __esmMin((() => {
975
1061
  init_console_capture();
976
1062
  function safe(name, fallback = "unknown") {
977
1063
  switch (name) {
978
- case "version": return "0.1.13";
979
- case "buildCommit": return "b3ee727cb";
980
- case "sdkVersion": return "0.1.13";
981
- case "sdkBundleHash": return "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
982
- case "sharedVersion": return "0.1.13";
983
- case "sharedBundleHash": return "36a23a454fd75ec35f258d39f254543ec0451c6b543fe3618ce427e6e6369aae";
984
- case "engineVersion": return "0.1.13";
985
- case "engineBundleHash": return "4ab08e63bcb8558b1191c5d3ab8d58b71950b77d911af56c9955ae1d787c4302";
1064
+ case "version": return "0.1.15";
1065
+ case "buildCommit": return "831f6a763";
1066
+ case "sdkVersion": return "0.1.15";
1067
+ case "sdkBundleHash": return "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
1068
+ case "sharedVersion": return "0.1.15";
1069
+ case "sharedBundleHash": return "e82a8b68a7d72698674352158c5de4c0d10824c61bfa7166cc8f846044b8b5e4";
1070
+ case "engineVersion": return "0.1.15";
1071
+ case "engineBundleHash": return "032d6b28b3d32bcbea80ba80a52934b017ee2c86a97fcaf8fe8db0d86d72a7f7";
986
1072
  default: return fallback;
987
1073
  }
988
1074
  }
@@ -1203,6 +1289,42 @@ function verifyNpmAvailable() {
1203
1289
  }
1204
1290
  var init_npm_resolver = __esmMin((() => {}));
1205
1291
  //#endregion
1292
+ //#region src/execution/tmp-gc.ts
1293
+ init_npm_resolver();
1294
+ /**
1295
+ * Startup garbage collection for this agent's own temp-directory families.
1296
+ *
1297
+ * Job workdirs (`kici-<6 random chars>`, see job-runner.ts) and isolated
1298
+ * pnpm stores (`kici-pnpm-store-*`, see dep-installer.ts) clean themselves
1299
+ * up in `finally` blocks — but a hard process death (SIGKILL, OOM kill)
1300
+ * skips those, and on a long-lived bare-metal agent the leftovers then
1301
+ * accumulate forever. Collecting anything older than a day at startup is
1302
+ * safe on shared hosts: no job lives remotely that long (job timeouts are
1303
+ * minutes), so a concurrent agent's in-flight dirs are never eligible.
1304
+ */
1305
+ const AGENT_TMP_GC_MAX_AGE_MS = 1440 * 60 * 1e3;
1306
+ /** mkdtemp's 6-char suffix on the bare `kici-` prefix — job workdirs only. */
1307
+ const AGENT_WORKDIR_PATTERN = /^kici-[A-Za-z0-9]{6}$/;
1308
+ const PNPM_STORE_PATTERN = /^kici-pnpm-store-/;
1309
+ /**
1310
+ * Collect this agent's stale temp dirs. `base` is overridable for tests;
1311
+ * production callers use the default temp root. Never throws.
1312
+ */
1313
+ async function gcStaleAgentTmpDirs(base = tmpdir()) {
1314
+ const log = (m) => logger.info(m);
1315
+ return [...await gcStaleTmpDirs({
1316
+ base,
1317
+ pattern: AGENT_WORKDIR_PATTERN,
1318
+ maxAgeMs: AGENT_TMP_GC_MAX_AGE_MS,
1319
+ log
1320
+ }), ...await gcStaleTmpDirs({
1321
+ base,
1322
+ pattern: PNPM_STORE_PATTERN,
1323
+ maxAgeMs: AGENT_TMP_GC_MAX_AGE_MS,
1324
+ log
1325
+ })];
1326
+ }
1327
+ //#endregion
1206
1328
  //#region src/metrics/prometheus.ts
1207
1329
  var prometheus_exports = /* @__PURE__ */ __exportAll({
1208
1330
  cloneDurationSeconds: () => cloneDurationSeconds,
@@ -1462,7 +1584,8 @@ var init_git_clone = __esmMin((() => {
1462
1584
  //#region src/execution/workflow-loader.ts
1463
1585
  /**
1464
1586
  * Workflow module loading: transforms `.ts` workflow files on import via the
1465
- * shared oxc-transform ESM loader hook. Customer workflow code is imported
1587
+ * `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook. Customer
1588
+ * workflow code is imported
1466
1589
  * directly from the cloned / extracted source tree — no intermediate bundle,
1467
1590
  * no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
1468
1591
  * Node's normal ESM lookup against `.kici/node_modules/`.
@@ -1478,7 +1601,7 @@ var workflow_loader_exports = /* @__PURE__ */ __exportAll({
1478
1601
  });
1479
1602
  function ensureLoaderHookRegistered() {
1480
1603
  if (hookRegistered) return;
1481
- register("@kici-dev/shared/ts-loader-hook", import.meta.url);
1604
+ register("@kici-dev/core/ts-loader-hook", import.meta.url);
1482
1605
  hookRegistered = true;
1483
1606
  }
1484
1607
  /**
@@ -1629,8 +1752,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1629
1752
  }
1630
1753
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1631
1754
  var init_workflow_loader = __esmMin((() => {
1632
- AGENT_SDK_VERSION = "0.1.13";
1633
- AGENT_SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
1755
+ AGENT_SDK_VERSION = "0.1.15";
1756
+ AGENT_SDK_BUNDLE_HASH = "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
1634
1757
  hookRegistered = false;
1635
1758
  }));
1636
1759
  //#endregion
@@ -1651,7 +1774,7 @@ var init_workflow_loader = __esmMin((() => {
1651
1774
  async function packKiciSource(workDir) {
1652
1775
  const kiciDir = join(workDir, ".kici");
1653
1776
  if (!existsSync(kiciDir)) throw new Error(`.kici/ not found at ${kiciDir}`);
1654
- logger$9.info("Packing .kici/ source tarball", { dir: workDir });
1777
+ logger$10.info("Packing .kici/ source tarball", { dir: workDir });
1655
1778
  const startTime = Date.now();
1656
1779
  const stream = c({
1657
1780
  gzip: true,
@@ -1665,7 +1788,7 @@ async function packKiciSource(workDir) {
1665
1788
  const hash = sha256(tarball);
1666
1789
  const sizeKB = (tarball.length / 1024).toFixed(2);
1667
1790
  const durationMs = Date.now() - startTime;
1668
- logger$9.info(".kici/ source packed", {
1791
+ logger$10.info(".kici/ source packed", {
1669
1792
  sizeKB,
1670
1793
  hash: hash.slice(0, 12),
1671
1794
  durationMs
@@ -1675,9 +1798,9 @@ async function packKiciSource(workDir) {
1675
1798
  hash
1676
1799
  };
1677
1800
  }
1678
- var logger$9;
1801
+ var logger$10;
1679
1802
  var init_source_packer = __esmMin((() => {
1680
- logger$9 = createLogger({ prefix: "source-packer" });
1803
+ logger$10 = createLogger({ prefix: "source-packer" });
1681
1804
  }));
1682
1805
  //#endregion
1683
1806
  //#region src/execution/dep-restore.ts
@@ -1818,7 +1941,7 @@ async function cleanupScratch(scratchDir) {
1818
1941
  force: true
1819
1942
  });
1820
1943
  } catch (cleanupErr) {
1821
- logger$8.warn("Scratch dir cleanup failed (orphan left behind)", {
1944
+ logger$9.warn("Scratch dir cleanup failed (orphan left behind)", {
1822
1945
  scratchDir,
1823
1946
  error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
1824
1947
  });
@@ -1843,7 +1966,7 @@ async function cleanupScratch(scratchDir) {
1843
1966
  */
1844
1967
  async function restoreDeps(workDir, depsUrl, depsHash) {
1845
1968
  depsUrl = resolveOrchestratorUrl(depsUrl);
1846
- logger$8.info("Downloading dependency tarball", { url: depsUrl });
1969
+ logger$9.info("Downloading dependency tarball", { url: depsUrl });
1847
1970
  const kiciDir = join(workDir, ".kici");
1848
1971
  if (depsUrl.startsWith("file://")) {
1849
1972
  const localPath = fileURLToPath(depsUrl);
@@ -1857,7 +1980,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1857
1980
  await moveScratchIntoRepo(scratchDir, workDir);
1858
1981
  await cleanupScratch(scratchDir);
1859
1982
  const sizeMB = (data.length / (1024 * 1024)).toFixed(2);
1860
- logger$8.info("Dependencies restored from cache (file)", {
1983
+ logger$9.info("Dependencies restored from cache (file)", {
1861
1984
  sizeMB,
1862
1985
  targetDir: workDir
1863
1986
  });
@@ -1866,7 +1989,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1866
1989
  if (!depsUrl.startsWith("http://") && !depsUrl.startsWith("https://")) throw new Error(`Unsupported deps URL scheme: ${depsUrl}`);
1867
1990
  let lastError;
1868
1991
  for (let attempt = 0; attempt <= 2; attempt++) {
1869
- if (attempt > 0) logger$8.warn("Retrying dep tarball download", {
1992
+ if (attempt > 0) logger$9.warn("Retrying dep tarball download", {
1870
1993
  attempt,
1871
1994
  url: depsUrl
1872
1995
  });
@@ -1875,11 +1998,11 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1875
1998
  if (depsHash && hash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${hash}`);
1876
1999
  await moveScratchIntoRepo(scratchDir, workDir);
1877
2000
  await cleanupScratch(scratchDir);
1878
- logger$8.info("Dependencies restored from cache (stream)", { targetDir: workDir });
2001
+ logger$9.info("Dependencies restored from cache (stream)", { targetDir: workDir });
1879
2002
  return;
1880
2003
  } catch (err) {
1881
2004
  lastError = err instanceof Error ? err : new Error(String(err));
1882
- logger$8.warn("Dep tarball download failed", {
2005
+ logger$9.warn("Dep tarball download failed", {
1883
2006
  attempt,
1884
2007
  error: lastError.message
1885
2008
  });
@@ -1887,9 +2010,9 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1887
2010
  }
1888
2011
  throw new Error(`Dep tarball download failed after 3 attempts: ${lastError?.message}`);
1889
2012
  }
1890
- var logger$8, DOWNLOAD_TIMEOUT_MS$1, SCRATCH_DIR_BASENAME_PREFIX;
2013
+ var logger$9, DOWNLOAD_TIMEOUT_MS$1, SCRATCH_DIR_BASENAME_PREFIX;
1891
2014
  var init_dep_restore = __esmMin((() => {
1892
- logger$8 = createLogger({ prefix: "dep-restore" });
2015
+ logger$9 = createLogger({ prefix: "dep-restore" });
1893
2016
  DOWNLOAD_TIMEOUT_MS$1 = 300 * 1e3;
1894
2017
  SCRATCH_DIR_BASENAME_PREFIX = ".dep-restore-scratch-";
1895
2018
  `${SCRATCH_DIR_BASENAME_PREFIX}`;
@@ -2001,7 +2124,7 @@ async function extractSourceTarball(data, targetDir) {
2001
2124
  }
2002
2125
  async function restoreSource(workDir, sourceTarUrl) {
2003
2126
  sourceTarUrl = resolveOrchestratorUrl(sourceTarUrl);
2004
- logger$7.info("Restoring .kici/ source from tarball", { sourceTarUrl });
2127
+ logger$8.info("Restoring .kici/ source from tarball", { sourceTarUrl });
2005
2128
  const startTime = Date.now();
2006
2129
  let data;
2007
2130
  if (sourceTarUrl.startsWith("file://")) {
@@ -2011,16 +2134,16 @@ async function restoreSource(workDir, sourceTarUrl) {
2011
2134
  else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
2012
2135
  await extractSourceTarball(data, workDir);
2013
2136
  const durationMs = Date.now() - startTime;
2014
- logger$7.info(".kici/ source restored", {
2137
+ logger$8.info(".kici/ source restored", {
2015
2138
  sizeKB: (data.length / 1024).toFixed(2),
2016
2139
  durationMs
2017
2140
  });
2018
2141
  }
2019
- var logger$7;
2142
+ var logger$8;
2020
2143
  var init_source_restore = __esmMin((() => {
2021
2144
  init_download();
2022
2145
  init_dep_restore();
2023
- logger$7 = createLogger({ prefix: "source-restore" });
2146
+ logger$8 = createLogger({ prefix: "source-restore" });
2024
2147
  }));
2025
2148
  //#endregion
2026
2149
  //#region src/execution/timeout-util.ts
@@ -2068,7 +2191,7 @@ function findJobByName(workflow, jobName) {
2068
2191
  *
2069
2192
  * @param workflow - The extracted Workflow object
2070
2193
  * @param jobName - Name of the job whose dynamic fields to evaluate
2071
- * @param event - Normalized webhook event data, passed as argument to dynamic functions
2194
+ * @param event - Normalized event envelope — same shape every dynamic-function call site receives.
2072
2195
  * @param flags - Which fields are dynamic and need evaluation
2073
2196
  * @param timeoutMs - Timeout per dynamic function call (default 60_000ms)
2074
2197
  */
@@ -2257,7 +2380,12 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2257
2380
  log: ctx.log,
2258
2381
  env: ctx.env
2259
2382
  };
2260
- const values = await withTimeout(() => matrix(matrixCtx), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic matrix for generated job '${jobName}'`);
2383
+ let values;
2384
+ try {
2385
+ values = await withTimeout(() => matrix(matrixCtx), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic matrix for generated job '${jobName}'`);
2386
+ } catch (err) {
2387
+ throw new MatrixExpansionError(jobName, `Matrix expansion failed for job '${jobName}': ${err.message}`);
2388
+ }
2261
2389
  if (Array.isArray(values)) return {
2262
2390
  _type: "static",
2263
2391
  values
@@ -2266,11 +2394,19 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2266
2394
  _type: "static",
2267
2395
  values
2268
2396
  };
2269
- throw new Error(`Job '${jobName}': dynamic matrix function returned an unsupported value (expected array or object, got ${typeof values})`);
2397
+ throw new MatrixExpansionError(jobName, `Job '${jobName}': dynamic matrix function returned an unsupported value (expected array or object, got ${typeof values})`);
2270
2398
  }
2271
- var DYNAMIC_FIELD_TIMEOUT_MS;
2399
+ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
2272
2400
  var init_dynamic_job_serializer = __esmMin((() => {
2273
2401
  init_timeout_util();
2402
+ MatrixExpansionError = class MatrixExpansionError extends Error {
2403
+ name = "MatrixExpansionError";
2404
+ constructor(jobName, message) {
2405
+ super(message);
2406
+ this.jobName = jobName;
2407
+ Object.setPrototypeOf(this, MatrixExpansionError.prototype);
2408
+ }
2409
+ };
2274
2410
  DYNAMIC_FIELD_TIMEOUT_MS = 6e4;
2275
2411
  })), DEFAULT_MAX_LOG_SIZE_BYTES, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_FLUSH_LINE_THRESHOLD, PAUSE_SAFETY_TIMEOUT_MS, LogStreamer;
2276
2412
  var init_log_streamer = __esmMin((() => {
@@ -2539,7 +2675,7 @@ async function applyOverlay(config) {
2539
2675
  const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
2540
2676
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2541
2677
  try {
2542
- logger$6.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2678
+ logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2543
2679
  let encryptedData;
2544
2680
  try {
2545
2681
  encryptedData = await downloadUrl(tarballUrl);
@@ -2549,7 +2685,7 @@ async function applyOverlay(config) {
2549
2685
  const cliPubKeyBuf = Buffer.from(cliPublicKey, "base64");
2550
2686
  const aesKey = deriveSharedSecret(Buffer.from(orchestratorPrivateKey, "base64"), cliPubKeyBuf);
2551
2687
  const decryptedData = decryptBuffer(encryptedData, aesKey);
2552
- logger$6.info("Extracting overlay tarball", { size: decryptedData.length });
2688
+ logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
2553
2689
  const extractDir = path.join(tmpDir, "extracted");
2554
2690
  await fs.mkdir(extractDir, { recursive: true });
2555
2691
  try {
@@ -2598,10 +2734,10 @@ async function applyOverlay(config) {
2598
2734
  await fs.unlink(targetPath);
2599
2735
  filesDeleted++;
2600
2736
  } catch {
2601
- logger$6.debug("Deletion target not found, skipping", { file });
2737
+ logger$7.debug("Deletion target not found, skipping", { file });
2602
2738
  }
2603
2739
  }
2604
- logger$6.info("Overlay applied successfully", {
2740
+ logger$7.info("Overlay applied successfully", {
2605
2741
  filesApplied,
2606
2742
  filesDeleted
2607
2743
  });
@@ -2617,10 +2753,10 @@ async function applyOverlay(config) {
2617
2753
  }).catch(() => {});
2618
2754
  }
2619
2755
  }
2620
- var logger$6, IV_LENGTH$1, AUTH_TAG_LENGTH;
2756
+ var logger$7, IV_LENGTH$1, AUTH_TAG_LENGTH;
2621
2757
  var init_overlay_applier = __esmMin((() => {
2622
2758
  init_download();
2623
- logger$6 = createLogger({ prefix: "overlay-applier" });
2759
+ logger$7 = createLogger({ prefix: "overlay-applier" });
2624
2760
  IV_LENGTH$1 = 12;
2625
2761
  AUTH_TAG_LENGTH = 16;
2626
2762
  }));
@@ -2934,7 +3070,7 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
2934
3070
  async function installDeps(kiciDir, opts = {}) {
2935
3071
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
2936
3072
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
2937
- logger$5.info("Installing deps inline", {
3073
+ logger$6.info("Installing deps inline", {
2938
3074
  packageManager,
2939
3075
  dir: kiciDir
2940
3076
  });
@@ -2975,7 +3111,7 @@ async function installDeps(kiciDir, opts = {}) {
2975
3111
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
2976
3112
  const durationMs = Date.now() - startTime;
2977
3113
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
2978
- logger$5.info("Deps installed inline", {
3114
+ logger$6.info("Deps installed inline", {
2979
3115
  packageManager,
2980
3116
  durationMs
2981
3117
  });
@@ -3041,7 +3177,8 @@ async function runPnpmInstall(args) {
3041
3177
  `--config.store-dir=${storeDir}`,
3042
3178
  "--config.package-import-method=copy",
3043
3179
  "--config.confirm-modules-purge=false",
3044
- "--config.side-effects-cache=false"
3180
+ "--config.side-effects-cache=false",
3181
+ PNPM_IGNORE_BUILD_GATE_ARG
3045
3182
  ];
3046
3183
  if (args.hasPrivateRegistry) argv.push("--ignore-scripts");
3047
3184
  try {
@@ -3112,12 +3249,11 @@ function logSubprocessStreams(e, tokens) {
3112
3249
  if (e && typeof e === "object" && "stdout" in e) process.stderr.write(`[dep-installer:trace] stdout: ${redactNpmOutput(String(e.stdout), tokens).slice(0, 500)}\n`);
3113
3250
  if (e && typeof e === "object" && "stderr" in e) process.stderr.write(`[dep-installer:trace] stderr: ${redactNpmOutput(String(e.stderr), tokens).slice(0, 500)}\n`);
3114
3251
  }
3115
- var logger$5, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3252
+ var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3116
3253
  var init_dep_installer = __esmMin((() => {
3117
- init_npm_resolver();
3118
3254
  init_npm_registry_config();
3119
3255
  init_validate_kici_deps();
3120
- logger$5 = createLogger({ prefix: "dep-installer" });
3256
+ logger$6 = createLogger({ prefix: "dep-installer" });
3121
3257
  execFileAsync = promisify(execFile);
3122
3258
  INSTALL_TIMEOUT_MS = 6e5;
3123
3259
  INSTALL_MAX_BUFFER = 128 * 1024 * 1024;
@@ -3156,7 +3292,7 @@ async function packNodeModules(kiciDir) {
3156
3292
  const workDir = dirname(kiciDir);
3157
3293
  const packageManager = await detectPackageManagerFromManifests(workDir) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3158
3294
  const entries = await closureEntries(workDir, kiciDir, packageManager);
3159
- logger$4.info("Packing dependency closure into tarball", {
3295
+ logger$5.info("Packing dependency closure into tarball", {
3160
3296
  dir: workDir,
3161
3297
  packageManager,
3162
3298
  entries
@@ -3172,7 +3308,7 @@ async function packNodeModules(kiciDir) {
3172
3308
  const tarball = Buffer.concat(chunks);
3173
3309
  const hash = sha256(tarball);
3174
3310
  const sizeMB = (tarball.length / (1024 * 1024)).toFixed(2);
3175
- logger$4.info("Dependency closure packed", {
3311
+ logger$5.info("Dependency closure packed", {
3176
3312
  sizeMB,
3177
3313
  hash: hash.slice(0, 12),
3178
3314
  durationMs: Date.now() - startTime
@@ -3260,9 +3396,9 @@ function isInside(root, target) {
3260
3396
  function isAbsoluteRel(rel) {
3261
3397
  return rel.length > 1 && rel[1] === ":";
3262
3398
  }
3263
- var logger$4;
3399
+ var logger$5;
3264
3400
  var init_dep_packer = __esmMin((() => {
3265
- logger$4 = createLogger({ prefix: "dep-packer" });
3401
+ logger$5 = createLogger({ prefix: "dep-packer" });
3266
3402
  }));
3267
3403
  //#endregion
3268
3404
  //#region src/execution/sandbox/env-sanitizer.ts
@@ -3425,6 +3561,7 @@ function buildRequest(dispatch, workDir) {
3425
3561
  contentHash: jobConfig.contentHash,
3426
3562
  resolvedHashFiles: jobConfig.resolvedHashFiles,
3427
3563
  maxLogSizeBytes: dispatch.maxLogSizeBytes,
3564
+ jobTimeoutMs: jobConfig.timeout,
3428
3565
  container: jobConfig.container,
3429
3566
  event: jobConfig.event,
3430
3567
  provider: jobConfig.provider,
@@ -3666,6 +3803,24 @@ function relayAgentApiRequest(msg, ctx) {
3666
3803
  error: toErrorMessage(err)
3667
3804
  }));
3668
3805
  }
3806
+ /** Relay `cache.request` and pipe the orchestrator response (or an error
3807
+ * response, or a "not configured" response when the agent didn't provide
3808
+ * the callback) back into the sandbox runner. */
3809
+ function relayCacheRequest$1(msg, ctx) {
3810
+ if (!ctx.execOptions.onCacheRequest) {
3811
+ safeSendToChild(ctx.child, {
3812
+ type: "cache.response",
3813
+ requestId: msg.requestId,
3814
+ error: "Cache not available in this agent configuration"
3815
+ });
3816
+ return;
3817
+ }
3818
+ ctx.execOptions.onCacheRequest(msg).then((response) => safeSendToChild(ctx.child, response), (err) => safeSendToChild(ctx.child, {
3819
+ type: "cache.response",
3820
+ requestId: msg.requestId,
3821
+ error: toErrorMessage(err)
3822
+ }));
3823
+ }
3669
3824
  /** Resolve the result promise for `job.complete` IPC messages. Encrypts
3670
3825
  * secret outputs (when a runPublicKey is available) and overrides status to
3671
3826
  * `cancelled` if a cancel was already in flight. */
@@ -3712,7 +3867,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
3712
3867
  ctx.execOptions.onStepStatus(msg.stepIndex, ctx.stepNames.get(msg.stepIndex) ?? "", msg.status, {
3713
3868
  durationMs: msg.durationMs,
3714
3869
  ...msg.error && { error: msg.error },
3715
- ...msg.secretsAccessed && { secretsAccessed: msg.secretsAccessed }
3870
+ ...msg.secretsAccessed && { secretsAccessed: msg.secretsAccessed },
3871
+ ...msg.step_type && { step_type: msg.step_type },
3872
+ ...msg.data && msg.data
3716
3873
  });
3717
3874
  return;
3718
3875
  case "step.secret_mount":
@@ -3733,6 +3890,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
3733
3890
  case "agent.api.request":
3734
3891
  relayAgentApiRequest(msg, ctx);
3735
3892
  return;
3893
+ case "cache.request":
3894
+ relayCacheRequest$1(msg, ctx);
3895
+ return;
3736
3896
  case "job.complete":
3737
3897
  handleJobComplete(msg, dispatch, ctx);
3738
3898
  return;
@@ -3804,6 +3964,7 @@ function buildCancelFn(child, ctx, defaultGracePeriodMs, agentMaxGracePeriodMs)
3804
3964
  */
3805
3965
  function createForkRunner(options, execOptions) {
3806
3966
  const sanitizedEnv = buildSanitizedEnv(options.env);
3967
+ if (options.useBwrap) sanitizedEnv.TMPDIR = "/tmp";
3807
3968
  const effectiveWorkDir = options.workDir ?? "/workspace";
3808
3969
  const dispatch = execOptions.dispatch;
3809
3970
  const { child, pidAssigned } = spawnRunnerChild(options, sanitizedEnv, effectiveWorkDir);
@@ -3902,10 +4063,10 @@ var init_fork_runner = __esmMin((() => {
3902
4063
  * network access. This mode provides credential isolation only and should
3903
4064
  * be used in trusted environments.
3904
4065
  */
3905
- var logger$3, BareMetalSandbox;
4066
+ var logger$4, BareMetalSandbox;
3906
4067
  var init_bare_metal_sandbox = __esmMin((() => {
3907
4068
  init_fork_runner();
3908
- logger$3 = createLogger({ prefix: "bare-metal-sandbox" });
4069
+ logger$4 = createLogger({ prefix: "bare-metal-sandbox" });
3909
4070
  BareMetalSandbox = class {
3910
4071
  runnerPath;
3911
4072
  useBwrap;
@@ -3932,12 +4093,12 @@ var init_bare_metal_sandbox = __esmMin((() => {
3932
4093
  if (this.useBwrap) try {
3933
4094
  const { execSync } = await import("node:child_process");
3934
4095
  execSync("which bwrap", { stdio: "ignore" });
3935
- if (this.sandboxNetwork === "isolated") logger$3.info("Bubblewrap (bwrap) sandbox enabled with network isolation (--unshare-net)");
3936
- else logger$3.info("Bubblewrap (bwrap) sandbox enabled with host network (KICI_SANDBOX_NETWORK=host)");
4096
+ if (this.sandboxNetwork === "isolated") logger$4.info("Bubblewrap (bwrap) sandbox enabled with network isolation (--unshare-net)");
4097
+ else logger$4.info("Bubblewrap (bwrap) sandbox enabled with host network (KICI_SANDBOX_NETWORK=host)");
3937
4098
  } catch {
3938
4099
  throw new Error("Bubblewrap (bwrap) not found. Install bubblewrap or set sandbox=false. On Debian/Ubuntu: apt install bubblewrap");
3939
4100
  }
3940
- else logger$3.warn("Bare-metal without sandbox provides limited isolation. Only environment sanitization is active. Enable sandbox=true with bubblewrap for PID/IPC/filesystem namespace isolation.");
4101
+ else logger$4.warn("Bare-metal without sandbox provides limited isolation. Only environment sanitization is active. Enable sandbox=true with bubblewrap for PID/IPC/filesystem namespace isolation.");
3941
4102
  }
3942
4103
  /**
3943
4104
  * Execute a job by forking the workflow runner with sanitized environment.
@@ -4127,6 +4288,32 @@ function relayApiRequest(stream, options, apiMsg) {
4127
4288
  } catch {}
4128
4289
  }
4129
4290
  /**
4291
+ * Relay cache.request from the container runner to the orchestrator via
4292
+ * options.onCacheRequest, then write the response back through `stream`. If
4293
+ * the agent doesn't expose a cache relay, write a structured error response
4294
+ * so the runner doesn't hang.
4295
+ */
4296
+ function relayCacheRequest(stream, options, cacheMsg) {
4297
+ const writeResponse = (response) => {
4298
+ try {
4299
+ stream.write(JSON.stringify(response) + "\n");
4300
+ } catch {}
4301
+ };
4302
+ if (!options.onCacheRequest) {
4303
+ writeResponse({
4304
+ type: "cache.response",
4305
+ requestId: cacheMsg.requestId,
4306
+ error: "Cache not available in this agent configuration"
4307
+ });
4308
+ return;
4309
+ }
4310
+ options.onCacheRequest(cacheMsg).then((response) => writeResponse(response), (err) => writeResponse({
4311
+ type: "cache.response",
4312
+ requestId: cacheMsg.requestId,
4313
+ error: toErrorMessage(err)
4314
+ }));
4315
+ }
4316
+ /**
4130
4317
  * Apply a job.complete message to the mutable runner state: capture status,
4131
4318
  * merge any bulk-reported step results, propagate plain outputs, and encrypt
4132
4319
  * secret outputs if a run public key is available.
@@ -4141,14 +4328,14 @@ function applyJobComplete(msg, stepResults, state, options) {
4141
4328
  if (msg.secretOutputs && options.dispatch.runPublicKey) try {
4142
4329
  state.encryptedSecretOutputs = encryptSecretOutputs(msg.secretOutputs, options.dispatch.runPublicKey);
4143
4330
  } catch (err) {
4144
- logger$2.warn("Failed to encrypt secret outputs", { error: toErrorMessage(err) });
4331
+ logger$3.warn("Failed to encrypt secret outputs", { error: toErrorMessage(err) });
4145
4332
  }
4146
4333
  }
4147
- var logger$2, MAX_STDERR_LINES, ABORT_GRACE_MS, CONTAINER_STOP_TIMEOUT, ContainerSandbox;
4334
+ var logger$3, MAX_STDERR_LINES, ABORT_GRACE_MS, CONTAINER_STOP_TIMEOUT, ContainerSandbox;
4148
4335
  var init_container_sandbox = __esmMin((() => {
4149
4336
  init_fork_runner();
4150
4337
  init_secret_encryption();
4151
- logger$2 = createLogger({ prefix: "container-sandbox" });
4338
+ logger$3 = createLogger({ prefix: "container-sandbox" });
4152
4339
  MAX_STDERR_LINES = 20;
4153
4340
  ABORT_GRACE_MS = 1e4;
4154
4341
  CONTAINER_STOP_TIMEOUT = 10;
@@ -4180,7 +4367,7 @@ var init_container_sandbox = __esmMin((() => {
4180
4367
  async setup(options) {
4181
4368
  this.containerName = `kici-sandbox-${this.jobId}-${Date.now()}`;
4182
4369
  const envArray = Object.entries(this.env).map(([k, v]) => `${k}=${v}`);
4183
- logger$2.info("Creating sandbox container", {
4370
+ logger$3.info("Creating sandbox container", {
4184
4371
  name: this.containerName,
4185
4372
  image: this.image,
4186
4373
  workDir: options.workDir
@@ -4198,7 +4385,7 @@ var init_container_sandbox = __esmMin((() => {
4198
4385
  HostConfig: { Binds: [`${options.workDir}:/workspace`, `${this.runnerPath}:${this.runnerMountPath}:ro`] }
4199
4386
  });
4200
4387
  await this.container.start();
4201
- logger$2.info("Sandbox container started", {
4388
+ logger$3.info("Sandbox container started", {
4202
4389
  name: this.containerName,
4203
4390
  containerId: this.container.id.slice(0, 12)
4204
4391
  });
@@ -4211,7 +4398,7 @@ var init_container_sandbox = __esmMin((() => {
4211
4398
  try {
4212
4399
  outcome = await this.awaitJobCompletion(streamCtx, options);
4213
4400
  } catch (err) {
4214
- logger$2.error("Job execution error", {
4401
+ logger$3.error("Job execution error", {
4215
4402
  error: toErrorMessage(err),
4216
4403
  stderrTail: streamCtx.stderrLines.slice(-5).join("\n")
4217
4404
  });
@@ -4261,7 +4448,7 @@ var init_container_sandbox = __esmMin((() => {
4261
4448
  });
4262
4449
  const abortHandler = () => {
4263
4450
  this.handleAbort().catch((err) => {
4264
- logger$2.warn("Error during abort", { error: toErrorMessage(err) });
4451
+ logger$3.warn("Error during abort", { error: toErrorMessage(err) });
4265
4452
  });
4266
4453
  };
4267
4454
  options.signal.addEventListener("abort", abortHandler, { once: true });
@@ -4298,7 +4485,7 @@ var init_container_sandbox = __esmMin((() => {
4298
4485
  try {
4299
4486
  msg = JSON.parse(line);
4300
4487
  } catch {
4301
- logger$2.warn("Non-JSON output from runner", { line: line.slice(0, 200) });
4488
+ logger$3.warn("Non-JSON output from runner", { line: line.slice(0, 200) });
4302
4489
  return;
4303
4490
  }
4304
4491
  if (this.dispatchRunnerMessage(msg, stream, options, stepNames, stepResults, state)) resolve({
@@ -4346,7 +4533,9 @@ var init_container_sandbox = __esmMin((() => {
4346
4533
  options.onStepStatus(msg.stepIndex, name, msg.status, {
4347
4534
  durationMs: msg.durationMs,
4348
4535
  ...msg.error && { error: msg.error },
4349
- ...msg.secretsAccessed && { secretsAccessed: msg.secretsAccessed }
4536
+ ...msg.secretsAccessed && { secretsAccessed: msg.secretsAccessed },
4537
+ ...msg.step_type && { step_type: msg.step_type },
4538
+ ...msg.data && msg.data
4350
4539
  });
4351
4540
  stepResults.push({
4352
4541
  name,
@@ -4378,11 +4567,14 @@ var init_container_sandbox = __esmMin((() => {
4378
4567
  case "agent.api.request":
4379
4568
  relayApiRequest(stream, options, msg);
4380
4569
  return false;
4570
+ case "cache.request":
4571
+ relayCacheRequest(stream, options, msg);
4572
+ return false;
4381
4573
  case "job.complete":
4382
4574
  applyJobComplete(msg, stepResults, state, options);
4383
4575
  return true;
4384
4576
  default:
4385
- logger$2.warn("Unrecognized IPC message from container runner", { type: msg.type });
4577
+ logger$3.warn("Unrecognized IPC message from container runner", { type: msg.type });
4386
4578
  return false;
4387
4579
  }
4388
4580
  }
@@ -4409,14 +4601,14 @@ var init_container_sandbox = __esmMin((() => {
4409
4601
  async teardown() {
4410
4602
  if (!this.container) return;
4411
4603
  if (this.keepFailed && this.jobFailed) {
4412
- logger$2.info("Keeping failed container for debugging", {
4604
+ logger$3.info("Keeping failed container for debugging", {
4413
4605
  name: this.containerName,
4414
4606
  containerId: this.container.id.slice(0, 12)
4415
4607
  });
4416
4608
  this.container = null;
4417
4609
  return;
4418
4610
  }
4419
- logger$2.info("Tearing down sandbox container", { name: this.containerName });
4611
+ logger$3.info("Tearing down sandbox container", { name: this.containerName });
4420
4612
  try {
4421
4613
  await this.container.stop({ t: CONTAINER_STOP_TIMEOUT });
4422
4614
  } catch {}
@@ -4447,7 +4639,7 @@ var init_container_sandbox = __esmMin((() => {
4447
4639
  */
4448
4640
  async handleAbort() {
4449
4641
  if (!this.execStream && !this.container) return;
4450
- logger$2.info("Aborting sandbox execution", { name: this.containerName });
4642
+ logger$3.info("Aborting sandbox execution", { name: this.containerName });
4451
4643
  if (this.execStream) try {
4452
4644
  this.execStream.write(JSON.stringify({ type: "abort" }) + "\n");
4453
4645
  } catch {}
@@ -4516,7 +4708,7 @@ function determineExecutionMode(jobConfig, agentConfig) {
4516
4708
  if (agentConfig.scalerManaged) return "firecracker";
4517
4709
  return "bare-metal";
4518
4710
  }
4519
- var logger$1, JobRunner$1;
4711
+ var logger$2, JobRunner$1;
4520
4712
  var init_job_runner = __esmMin((() => {
4521
4713
  init_git_clone();
4522
4714
  init_workflow_loader();
@@ -4533,7 +4725,7 @@ var init_job_runner = __esmMin((() => {
4533
4725
  init_download();
4534
4726
  init_sandbox();
4535
4727
  init_prometheus();
4536
- logger$1 = createLogger({ prefix: "job-runner" });
4728
+ logger$2 = createLogger({ prefix: "job-runner" });
4537
4729
  JobRunner$1 = class {
4538
4730
  send;
4539
4731
  sendDirect;
@@ -4547,6 +4739,7 @@ var init_job_runner = __esmMin((() => {
4547
4739
  _sendRunEvent;
4548
4740
  _sendConcurrencyReport;
4549
4741
  _sendApiRequest;
4742
+ _requestUserCache;
4550
4743
  /** Tracks running jobs for concurrency and cancellation */
4551
4744
  activeJobs = /* @__PURE__ */ new Map();
4552
4745
  /** Active sandbox for the current job (used for abort). */
@@ -4564,6 +4757,7 @@ var init_job_runner = __esmMin((() => {
4564
4757
  this._sendRunEvent = deps.sendRunEvent;
4565
4758
  this._sendConcurrencyReport = deps.sendConcurrencyReport;
4566
4759
  this._sendApiRequest = deps.sendApiRequest;
4760
+ this._requestUserCache = deps.requestUserCache;
4567
4761
  }
4568
4762
  /**
4569
4763
  * Execute a dispatched job through its full lifecycle.
@@ -4631,7 +4825,7 @@ var init_job_runner = __esmMin((() => {
4631
4825
  }
4632
4826
  if (jobConfig.buildOnly === true) {
4633
4827
  if (jobConfig.fullRepo) {
4634
- logger$1.warn("Build job received for fullRepo run -- skipping (should not happen)", {
4828
+ logger$2.warn("Build job received for fullRepo run -- skipping (should not happen)", {
4635
4829
  jobId,
4636
4830
  runId
4637
4831
  });
@@ -4653,7 +4847,7 @@ var init_job_runner = __esmMin((() => {
4653
4847
  async executeStandardJob(dispatch, workDir, abortController) {
4654
4848
  const { runId, jobId } = dispatch;
4655
4849
  const ctx = getRequestContext();
4656
- logger$1.info(`Run: ${ctx.runId ?? runId} | Trace: ${ctx.requestId ?? "N/A"}`);
4850
+ logger$2.info(`Run: ${ctx.runId ?? runId} | Trace: ${ctx.requestId ?? "N/A"}`);
4657
4851
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
4658
4852
  const heartbeatTimer = setInterval(() => {
4659
4853
  this.send({
@@ -4678,7 +4872,7 @@ var init_job_runner = __esmMin((() => {
4678
4872
  if (sandbox) {
4679
4873
  this.emitRunEvent(runId, "agent.teardown", { jobId });
4680
4874
  await sandbox.teardown().catch((err) => {
4681
- logger$1.warn("Sandbox teardown error", { error: toErrorMessage(err) });
4875
+ logger$2.warn("Sandbox teardown error", { error: toErrorMessage(err) });
4682
4876
  });
4683
4877
  }
4684
4878
  }
@@ -4706,7 +4900,7 @@ var init_job_runner = __esmMin((() => {
4706
4900
  environmentVars: typedConfig.environmentVars ?? void 0,
4707
4901
  jobEnv: typedConfig.jobEnv ?? void 0
4708
4902
  });
4709
- logger$1.info("Creating execution sandbox", {
4903
+ logger$2.info("Creating execution sandbox", {
4710
4904
  executionMode,
4711
4905
  jobId,
4712
4906
  runnerPath
@@ -4777,6 +4971,7 @@ var init_job_runner = __esmMin((() => {
4777
4971
  onStepStatus: (stepIndex, stepName, state, data) => {
4778
4972
  let logBytesStreamed;
4779
4973
  if (state === ExecutionStepStatus.enum.success || state === ExecutionStepStatus.enum.failed || state === ExecutionStepStatus.enum.skipped) logBytesStreamed = logStreamers.get(stepIndex)?.getTotalBytes() ?? 0;
4974
+ this.maybeEmitCacheRunEvent(runId, jobId, stepIndex, state, data);
4780
4975
  this.sendStepStatus(dispatch, stepIndex, stepName, state, data, logBytesStreamed);
4781
4976
  },
4782
4977
  onLogLine: (stepIndex, line) => {
@@ -4801,6 +4996,7 @@ var init_job_runner = __esmMin((() => {
4801
4996
  };
4802
4997
  },
4803
4998
  onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
4999
+ onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
4804
5000
  onSecretMount: (event) => {
4805
5001
  this.emitRunEvent(runId, "step.secret_mount", {
4806
5002
  jobId,
@@ -4834,7 +5030,7 @@ var init_job_runner = __esmMin((() => {
4834
5030
  stepsTotal.add(1, { status: stepResult.status });
4835
5031
  if (stepResult.durationMs > 0) stepDurationSeconds.record(stepResult.durationMs / 1e3);
4836
5032
  }
4837
- if (result.status === ExecutionJobStatus.enum.failed) logger$1.error("Sandbox returned failed result", {
5033
+ if (result.status === ExecutionJobStatus.enum.failed) logger$2.error("Sandbox returned failed result", {
4838
5034
  durationMs: result.durationMs,
4839
5035
  stepCount: result.stepResults.length,
4840
5036
  steps: result.stepResults.map((r) => `${r.name}:${r.status}`).join(","),
@@ -4863,7 +5059,7 @@ var init_job_runner = __esmMin((() => {
4863
5059
  async handleBuildJob(dispatch, workDir, abortController) {
4864
5060
  const { runId, jobId, jobConfig } = dispatch;
4865
5061
  const buildCtx = getRequestContext();
4866
- logger$1.info(`Run: ${buildCtx.runId ?? runId} | Trace: ${buildCtx.requestId ?? "N/A"}`);
5062
+ logger$2.info(`Run: ${buildCtx.runId ?? runId} | Trace: ${buildCtx.requestId ?? "N/A"}`);
4867
5063
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
4868
5064
  const buildStreamer = this.createStepStreamer(dispatch, 0);
4869
5065
  const buildLog = (msg) => buildStreamer.addLine(msg);
@@ -4952,14 +5148,14 @@ var init_job_runner = __esmMin((() => {
4952
5148
  const cliPublicKey = jobConfig.cliPublicKey;
4953
5149
  const orchestratorPrivateKey = jobConfig.orchestratorPrivateKey;
4954
5150
  if (tarballUrl && cliPublicKey && orchestratorPrivateKey) {
4955
- logger$1.info("Applying overlay tarball for test run", { jobId });
5151
+ logger$2.info("Applying overlay tarball for test run", { jobId });
4956
5152
  const overlayResult = await applyOverlay({
4957
5153
  tarballUrl,
4958
5154
  cliPublicKey,
4959
5155
  orchestratorPrivateKey,
4960
5156
  repoDir: workDir
4961
5157
  });
4962
- logger$1.info("Overlay applied", {
5158
+ logger$2.info("Overlay applied", {
4963
5159
  filesApplied: overlayResult.filesApplied,
4964
5160
  filesDeleted: overlayResult.filesDeleted
4965
5161
  });
@@ -4987,9 +5183,9 @@ var init_job_runner = __esmMin((() => {
4987
5183
  platform: os.platform(),
4988
5184
  arch: os.arch()
4989
5185
  };
4990
- logger$1.info("Requesting dep upload URL from orchestrator", { lockfileHash: buildConfig.lockfileHash });
5186
+ logger$2.info("Requesting dep upload URL from orchestrator", { lockfileHash: buildConfig.lockfileHash });
4991
5187
  const depUploadUrl = await this.requestUploadUrl(dispatch.jobId, "deps", depKey);
4992
- logger$1.info("Uploading dep tarball to S3", {
5188
+ logger$2.info("Uploading dep tarball to S3", {
4993
5189
  size: tarball.length,
4994
5190
  hash: hash.slice(0, 12)
4995
5191
  });
@@ -4998,7 +5194,7 @@ var init_job_runner = __esmMin((() => {
4998
5194
  ...depKey,
4999
5195
  depsHash: hash
5000
5196
  });
5001
- logger$1.info("Dep tarball upload complete", { lockfileHash: buildConfig.lockfileHash });
5197
+ logger$2.info("Dep tarball upload complete", { lockfileHash: buildConfig.lockfileHash });
5002
5198
  buildLog(`Deps tarball uploaded (${tarball.length} bytes)`);
5003
5199
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running, {
5004
5200
  buildEvent: "deps_packed",
@@ -5024,15 +5220,15 @@ var init_job_runner = __esmMin((() => {
5024
5220
  platform: os.platform(),
5025
5221
  arch: os.arch()
5026
5222
  };
5027
- logger$1.info("Requesting source tarball upload URL from orchestrator", { contentHash: buildConfig.contentHash });
5223
+ logger$2.info("Requesting source tarball upload URL from orchestrator", { contentHash: buildConfig.contentHash });
5028
5224
  const sourceUploadUrl = await this.requestUploadUrl(dispatch.jobId, "source", sourceKey);
5029
- logger$1.info("Uploading source tarball to S3", {
5225
+ logger$2.info("Uploading source tarball to S3", {
5030
5226
  size: tarball.length,
5031
5227
  contentHash: buildConfig.contentHash
5032
5228
  });
5033
5229
  await uploadToPresignedUrl(sourceUploadUrl, tarball);
5034
5230
  this.sendUploadComplete(dispatch.jobId, "source", sourceKey);
5035
- logger$1.info("Source tarball upload complete", { contentHash: buildConfig.contentHash });
5231
+ logger$2.info("Source tarball upload complete", { contentHash: buildConfig.contentHash });
5036
5232
  buildLog(`Source tarball packed and uploaded (${tarball.length} bytes, hash: ${buildConfig.contentHash.slice(0, 12)})`);
5037
5233
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running, {
5038
5234
  buildEvent: "source_packed",
@@ -5055,7 +5251,7 @@ var init_job_runner = __esmMin((() => {
5055
5251
  async handleInitJob(dispatch, workDir, abortController) {
5056
5252
  const { runId, jobId, jobConfig } = dispatch;
5057
5253
  const config = jobConfig;
5058
- logger$1.info("Starting init job", {
5254
+ logger$2.info("Starting init job", {
5059
5255
  jobId,
5060
5256
  targetJobName: config.targetJobName,
5061
5257
  workflowName: config.workflowName
@@ -5103,7 +5299,7 @@ var init_job_runner = __esmMin((() => {
5103
5299
  }
5104
5300
  const kiciDir = join(workDir, ".kici");
5105
5301
  const hasPackage = await fileExists(join(kiciDir, "package.json"));
5106
- logger$1.info("Init job: checking deps", {
5302
+ logger$2.info("Init job: checking deps", {
5107
5303
  kiciDir,
5108
5304
  hasPackageJson: hasPackage,
5109
5305
  source: config.source
@@ -5126,7 +5322,7 @@ var init_job_runner = __esmMin((() => {
5126
5322
  dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
5127
5323
  }, config.timeoutMs);
5128
5324
  });
5129
- logger$1.info("Init job completed successfully", {
5325
+ logger$2.info("Init job completed successfully", {
5130
5326
  jobId,
5131
5327
  hasEnvironment: initResult.environmentName !== void 0,
5132
5328
  hasEnv: initResult.env !== void 0,
@@ -5142,7 +5338,7 @@ var init_job_runner = __esmMin((() => {
5142
5338
  });
5143
5339
  } catch (err) {
5144
5340
  const errorMsg = toErrorMessage(err);
5145
- logger$1.error("Init job failed", {
5341
+ logger$2.error("Init job failed", {
5146
5342
  jobId,
5147
5343
  error: errorMsg
5148
5344
  });
@@ -5169,7 +5365,7 @@ var init_job_runner = __esmMin((() => {
5169
5365
  const { runId, jobId, jobConfig } = dispatch;
5170
5366
  const config = jobConfig;
5171
5367
  const timeoutMs = config.timeoutMs ?? 12e4;
5172
- logger$1.info("Starting DynamicJobFn evaluation", {
5368
+ logger$2.info("Starting DynamicJobFn evaluation", {
5173
5369
  jobId,
5174
5370
  workflowName: config.workflowName,
5175
5371
  sourceIndex: config.source.index
@@ -5270,7 +5466,7 @@ var init_job_runner = __esmMin((() => {
5270
5466
  workflowName: config.workflowName
5271
5467
  });
5272
5468
  });
5273
- logger$1.info("DynamicJobFn evaluation completed", {
5469
+ logger$2.info("DynamicJobFn evaluation completed", {
5274
5470
  jobId,
5275
5471
  generatedJobCount: lockJobs.length,
5276
5472
  jobNames: lockJobs.map((j) => j.name)
@@ -5285,7 +5481,7 @@ var init_job_runner = __esmMin((() => {
5285
5481
  });
5286
5482
  } catch (err) {
5287
5483
  const errorMsg = toErrorMessage(err);
5288
- logger$1.error("DynamicJobFn evaluation failed", {
5484
+ logger$2.error("DynamicJobFn evaluation failed", {
5289
5485
  jobId,
5290
5486
  error: errorMsg
5291
5487
  });
@@ -5293,10 +5489,17 @@ var init_job_runner = __esmMin((() => {
5293
5489
  await evalStreamer.flush();
5294
5490
  evalStreamer.destroy();
5295
5491
  this.sendStepStatus(dispatch, 0, "evaluate", ExecutionStepStatus.enum.failed, { error: errorMsg }, evalStreamer.getTotalBytes());
5296
- this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, {
5492
+ const dynamicData = {
5297
5493
  error: errorMsg,
5298
5494
  dynamicFailed: true
5299
- });
5495
+ };
5496
+ if (err instanceof MatrixExpansionError) dynamicData.initFailure = {
5497
+ scope: "job",
5498
+ category: InitFailureCategory.enum.matrix_expansion,
5499
+ message: errorMsg,
5500
+ jobName: err.jobName
5501
+ };
5502
+ this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, dynamicData);
5300
5503
  } finally {
5301
5504
  clearInterval(heartbeatTimer);
5302
5505
  }
@@ -5358,6 +5561,31 @@ var init_job_runner = __esmMin((() => {
5358
5561
  this._sendRunEvent(runId, eventType, opts);
5359
5562
  }
5360
5563
  /**
5564
+ * Emit a `cache.restore` / `cache.save` run event for a cache pseudo-step.
5565
+ *
5566
+ * The cache phase tags its `step.complete` IPC with a {@link CacheStepType}
5567
+ * `step_type` and a `data.cacheOutcome` ({@link CacheOutcome}); when one of
5568
+ * those terminal pseudo-step statuses arrives here, mirror it onto the run
5569
+ * timeline as a `run.event` so hit/miss/saved/skipped/error is recorded for
5570
+ * the dashboard. A no-op for regular steps and hooks.
5571
+ */
5572
+ maybeEmitCacheRunEvent(runId, jobId, stepIndex, state, data) {
5573
+ if (state === ExecutionStepStatus.enum.running) return;
5574
+ const stepType = data?.step_type;
5575
+ const eventType = stepType === CacheStepType.enum["cache:restore"] ? CacheRunEventType.enum["cache.restore"] : stepType === CacheStepType.enum["cache:save"] ? CacheRunEventType.enum["cache.save"] : void 0;
5576
+ if (!eventType) return;
5577
+ this.emitRunEvent(runId, eventType, {
5578
+ jobId,
5579
+ metadata: {
5580
+ stepIndex,
5581
+ ...data?.cacheOutcome !== void 0 && { outcome: data.cacheOutcome },
5582
+ ...data?.key !== void 0 && { key: data.key },
5583
+ ...data?.matchedKey !== void 0 && { matchedKey: data.matchedKey },
5584
+ ...data?.bytes !== void 0 && { bytes: data.bytes }
5585
+ }
5586
+ });
5587
+ }
5588
+ /**
5361
5589
  * Create a LogStreamer for a synthetic step (build, evaluate, etc.).
5362
5590
  */
5363
5591
  createStepStreamer(dispatch, stepIndex) {
@@ -5441,14 +5669,14 @@ var init_job_runner = __esmMin((() => {
5441
5669
  */
5442
5670
  init_console_capture();
5443
5671
  init_npm_resolver();
5444
- const AGENT_VERSION = "0.1.13";
5445
- const BUILD_COMMIT = "b3ee727cb";
5446
- const SDK_VERSION = "0.1.13";
5447
- const SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
5448
- const SHARED_VERSION = "0.1.13";
5449
- const SHARED_BUNDLE_HASH = "36a23a454fd75ec35f258d39f254543ec0451c6b543fe3618ce427e6e6369aae";
5450
- const ENGINE_VERSION = "0.1.13";
5451
- const ENGINE_BUNDLE_HASH = "4ab08e63bcb8558b1191c5d3ab8d58b71950b77d911af56c9955ae1d787c4302";
5672
+ const AGENT_VERSION = "0.1.15";
5673
+ const BUILD_COMMIT = "831f6a763";
5674
+ const SDK_VERSION = "0.1.15";
5675
+ const SDK_BUNDLE_HASH = "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
5676
+ const SHARED_VERSION = "0.1.15";
5677
+ const SHARED_BUNDLE_HASH = "e82a8b68a7d72698674352158c5de4c0d10824c61bfa7166cc8f846044b8b5e4";
5678
+ const ENGINE_VERSION = "0.1.15";
5679
+ const ENGINE_BUNDLE_HASH = "032d6b28b3d32bcbea80ba80a52934b017ee2c86a97fcaf8fe8db0d86d72a7f7";
5452
5680
  initTelemetry({
5453
5681
  serviceName: "kici-agent",
5454
5682
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -5456,18 +5684,19 @@ initTelemetry({
5456
5684
  const { connectionStatus, jobsActive, jobsTotal } = await Promise.resolve().then(() => (init_prometheus(), prometheus_exports));
5457
5685
  const { JobRunner } = await Promise.resolve().then(() => (init_job_runner(), job_runner_exports));
5458
5686
  setServiceName("agent");
5459
- const logger = createLogger({ prefix: "agent" });
5687
+ const logger$1 = createLogger({ prefix: "agent" });
5460
5688
  installConsoleCapture();
5461
- await guardStartup(logger, async () => {
5689
+ await guardStartup(logger$1, async () => {
5462
5690
  const config = loadConfig();
5463
- logger.info("Agent starting", {
5691
+ logger$1.info("Agent starting", {
5464
5692
  agentId: config.agentId,
5465
5693
  orchestratorUrl: config.orchestratorUrl,
5466
5694
  labels: config.labels,
5467
5695
  roles: config.roles,
5468
5696
  port: config.port
5469
5697
  });
5470
- logger.info("agent.build.info", {
5698
+ gcStaleAgentTmpDirs();
5699
+ logger$1.info("agent.build.info", {
5471
5700
  agentVersion: AGENT_VERSION,
5472
5701
  buildCommit: BUILD_COMMIT,
5473
5702
  sdkVersion: SDK_VERSION,
@@ -5489,7 +5718,7 @@ await guardStartup(logger, async () => {
5489
5718
  if (toolErrors.length > 0) throw new Error("Agent required-tools validation failed:\n" + toolErrors.map((e) => ` - ${e}`).join("\n"));
5490
5719
  if (config.roles === void 0 || config.roles.includes("builder")) {
5491
5720
  const npmVersion = verifyNpmAvailable();
5492
- logger.info("Builder role: npm verified", { npmVersion });
5721
+ logger$1.info("Builder role: npm verified", { npmVersion });
5493
5722
  }
5494
5723
  let isDraining = false;
5495
5724
  let idleShutdownTimer;
@@ -5506,7 +5735,8 @@ await guardStartup(logger, async () => {
5506
5735
  sendJobContext: (runId, jobId, context) => client.sendJobContext(runId, jobId, context),
5507
5736
  sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
5508
5737
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
5509
- sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {})
5738
+ sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
5739
+ requestUserCache: (jobId, request) => client.requestUserCache(jobId, request)
5510
5740
  });
5511
5741
  /** Build and send an agent.status message with dynamic OS metadata. */
5512
5742
  function sendAgentStatus() {
@@ -5530,15 +5760,31 @@ await guardStartup(logger, async () => {
5530
5760
  jobId: dispatch.jobId
5531
5761
  }, () => {
5532
5762
  if (isDraining) {
5533
- logger.info("Draining: rejecting job dispatch", { jobId: dispatch.jobId });
5763
+ logger$1.info("Draining: rejecting job dispatch", { jobId: dispatch.jobId });
5764
+ client.sendDirect({
5765
+ type: "job.reject",
5766
+ messageId: randomUUID(),
5767
+ runId: dispatch.runId,
5768
+ jobId: dispatch.jobId,
5769
+ reason: "draining",
5770
+ timestamp: Date.now()
5771
+ });
5534
5772
  sendAgentStatus();
5535
5773
  return;
5536
5774
  }
5537
5775
  if (jobRunner.activeJobs.size > 0) {
5538
- logger.warn("Already running a job, cannot accept another", {
5776
+ logger$1.warn("Already running a job, rejecting dispatch", {
5539
5777
  jobId: dispatch.jobId,
5540
5778
  activeJobs: jobRunner.activeJobs.size
5541
5779
  });
5780
+ client.sendDirect({
5781
+ type: "job.reject",
5782
+ messageId: randomUUID(),
5783
+ runId: dispatch.runId,
5784
+ jobId: dispatch.jobId,
5785
+ reason: "busy",
5786
+ timestamp: Date.now()
5787
+ });
5542
5788
  sendAgentStatus();
5543
5789
  return;
5544
5790
  }
@@ -5546,7 +5792,14 @@ await guardStartup(logger, async () => {
5546
5792
  clearTimeout(idleShutdownTimer);
5547
5793
  idleShutdownTimer = void 0;
5548
5794
  }
5549
- logger.info("Accepting job dispatch", {
5795
+ client.sendDirect({
5796
+ type: "job.ack",
5797
+ messageId: randomUUID(),
5798
+ runId: dispatch.runId,
5799
+ jobId: dispatch.jobId,
5800
+ timestamp: Date.now()
5801
+ });
5802
+ logger$1.info("Accepting job dispatch", {
5550
5803
  jobId: dispatch.jobId,
5551
5804
  runId: dispatch.runId,
5552
5805
  activeJobs: jobRunner.activeJobs.size + 1
@@ -5555,7 +5808,7 @@ await guardStartup(logger, async () => {
5555
5808
  jobRunner.execute(dispatch).then(() => {
5556
5809
  jobsTotal.add(1, { status: "success" });
5557
5810
  }).catch((err) => {
5558
- logger.error("Job execution error", {
5811
+ logger$1.error("Job execution error", {
5559
5812
  jobId: dispatch.jobId,
5560
5813
  error: toErrorMessage(err)
5561
5814
  });
@@ -5566,7 +5819,7 @@ await guardStartup(logger, async () => {
5566
5819
  sendAgentStatus();
5567
5820
  if (config.scalerManaged && jobRunner.activeJobs.size === 0) {
5568
5821
  if (client.state !== "registered") {
5569
- logger.info("Scaler-managed agent idle but disconnected, deferring shutdown until reconnected");
5822
+ logger$1.info("Scaler-managed agent idle but disconnected, deferring shutdown until reconnected");
5570
5823
  return;
5571
5824
  }
5572
5825
  startIdleShutdownTimer();
@@ -5575,7 +5828,7 @@ await guardStartup(logger, async () => {
5575
5828
  });
5576
5829
  },
5577
5830
  onJobCancel: (cancel) => {
5578
- logger.info("Job cancel received", {
5831
+ logger$1.info("Job cancel received", {
5579
5832
  jobId: cancel.jobId,
5580
5833
  reason: cancel.reason
5581
5834
  });
@@ -5593,13 +5846,13 @@ await guardStartup(logger, async () => {
5593
5846
  const idleMs = config.scalerIdleTimeoutMs;
5594
5847
  if (idleShutdownTimer) clearTimeout(idleShutdownTimer);
5595
5848
  if (idleMs <= 0) {
5596
- logger.info("Scaler-managed agent idle after job completion, shutting down");
5849
+ logger$1.info("Scaler-managed agent idle after job completion, shutting down");
5597
5850
  gracefulShutdown("scaler-idle");
5598
5851
  } else {
5599
- logger.info(`Scaler-managed agent idle, waiting ${idleMs}ms for follow-up jobs`);
5852
+ logger$1.info(`Scaler-managed agent idle, waiting ${idleMs}ms for follow-up jobs`);
5600
5853
  idleShutdownTimer = setTimeout(() => {
5601
5854
  if (jobRunner.activeJobs.size === 0) {
5602
- logger.info("Scaler-managed agent still idle after timeout, shutting down");
5855
+ logger$1.info("Scaler-managed agent still idle after timeout, shutting down");
5603
5856
  gracefulShutdown("scaler-idle");
5604
5857
  }
5605
5858
  }, idleMs);
@@ -5609,17 +5862,17 @@ await guardStartup(logger, async () => {
5609
5862
  if (!config.scalerManaged || jobRunner.activeJobs.size > 0) return;
5610
5863
  if (pendingDispatch) {
5611
5864
  const safetyMs = config.scalerPendingDispatchTimeoutMs;
5612
- logger.info(`Scaler-managed agent registered with pending bound dispatch, deferring idle shutdown for ${safetyMs}ms`);
5865
+ logger$1.info(`Scaler-managed agent registered with pending bound dispatch, deferring idle shutdown for ${safetyMs}ms`);
5613
5866
  if (idleShutdownTimer) clearTimeout(idleShutdownTimer);
5614
5867
  idleShutdownTimer = setTimeout(() => {
5615
5868
  if (jobRunner.activeJobs.size === 0) {
5616
- logger.warn("Scaler-managed agent pending-dispatch safety timeout exceeded, shutting down");
5869
+ logger$1.warn("Scaler-managed agent pending-dispatch safety timeout exceeded, shutting down");
5617
5870
  gracefulShutdown("scaler-pending-dispatch-timeout");
5618
5871
  }
5619
5872
  }, safetyMs);
5620
5873
  return;
5621
5874
  }
5622
- logger.info("Scaler-managed agent reconnected and idle, starting idle shutdown timer");
5875
+ logger$1.info("Scaler-managed agent reconnected and idle, starting idle shutdown timer");
5623
5876
  startIdleShutdownTimer();
5624
5877
  };
5625
5878
  const wsTransport = new winston.transports.Stream({
@@ -5629,14 +5882,14 @@ await guardStartup(logger, async () => {
5629
5882
  } }),
5630
5883
  format: winston.format.combine(winston.format.timestamp(), winston.format.json())
5631
5884
  });
5632
- logger.add(wsTransport);
5885
+ logger$1.add(wsTransport);
5633
5886
  const envProbes = {};
5634
5887
  for (const [k, v] of Object.entries(process.env)) {
5635
5888
  if (v === void 0) continue;
5636
5889
  if (!/_ENV_PROBE$|_ENV_PROBE_/.test(k)) continue;
5637
5890
  envProbes[k] = v.length <= 64 ? v : `${v.slice(0, 61)}...`;
5638
5891
  }
5639
- if (Object.keys(envProbes).length > 0) logger.info("Agent startup env probes (diagnostic)", envProbes);
5892
+ if (Object.keys(envProbes).length > 0) logger$1.info("Agent startup env probes (diagnostic)", envProbes);
5640
5893
  client.connect();
5641
5894
  connectionStatus.add(1);
5642
5895
  const metricsReporter = new MetricsReporter({
@@ -5676,13 +5929,13 @@ await guardStartup(logger, async () => {
5676
5929
  fetch: app.fetch,
5677
5930
  port: config.port
5678
5931
  }, (info) => {
5679
- logger.info(`Agent started on port ${info.port}`, {
5932
+ logger$1.info(`Agent started on port ${info.port}`, {
5680
5933
  port: info.port,
5681
5934
  agentId: config.agentId
5682
5935
  });
5683
5936
  });
5684
5937
  const { shutdown: gracefulShutdown } = setupGracefulShutdown({
5685
- logger,
5938
+ logger: logger$1,
5686
5939
  timeoutMs: 1e4,
5687
5940
  onForceExit: () => {
5688
5941
  for (const job of jobRunner.activeJobs.values()) job.abortController.abort();
@@ -5700,7 +5953,7 @@ await guardStartup(logger, async () => {
5700
5953
  name: "Waiting for active jobs to complete",
5701
5954
  fn: async () => {
5702
5955
  if (jobRunner.activeJobs.size > 0) {
5703
- logger.info("Active jobs remaining", { activeJobs: jobRunner.activeJobs.size });
5956
+ logger$1.info("Active jobs remaining", { activeJobs: jobRunner.activeJobs.size });
5704
5957
  await Promise.allSettled([...jobRunner.activeJobs.values()].map((j) => j.completionPromise));
5705
5958
  }
5706
5959
  }
@@ -5734,17 +5987,17 @@ await guardStartup(logger, async () => {
5734
5987
  ]
5735
5988
  });
5736
5989
  process.on("SIGUSR1", () => {
5737
- logger.info("Received SIGUSR1, entering drain mode");
5990
+ logger$1.info("Received SIGUSR1, entering drain mode");
5738
5991
  isDraining = true;
5739
5992
  if (jobRunner.activeJobs.size === 0) {
5740
- logger.info("No active jobs, shutting down immediately");
5993
+ logger$1.info("No active jobs, shutting down immediately");
5741
5994
  gracefulShutdown("SIGUSR1-drain");
5742
5995
  return;
5743
5996
  }
5744
5997
  const checkDrained = setInterval(() => {
5745
5998
  if (jobRunner.activeJobs.size === 0) {
5746
5999
  clearInterval(checkDrained);
5747
- logger.info("All jobs drained, shutting down");
6000
+ logger$1.info("All jobs drained, shutting down");
5748
6001
  gracefulShutdown("SIGUSR1-drain");
5749
6002
  }
5750
6003
  }, 1e3);