@kici-dev/agent 0.1.14 → 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";
@@ -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.14";
979
- case "buildCommit": return "0fc7a43c9";
980
- case "sdkVersion": return "0.1.14";
981
- case "sdkBundleHash": return "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
982
- case "sharedVersion": return "0.1.14";
983
- case "sharedBundleHash": return "5a44862f00d382dbac676c0bdbf9e3fb3016d1c4667b60ea4ae6cbf8ac8deaf4";
984
- case "engineVersion": return "0.1.14";
985
- case "engineBundleHash": return "8eef210908744b12e19a2b915520298c5dde5abd4a064fa75e87123e237bf932";
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,
@@ -1630,8 +1752,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1630
1752
  }
1631
1753
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1632
1754
  var init_workflow_loader = __esmMin((() => {
1633
- AGENT_SDK_VERSION = "0.1.14";
1634
- AGENT_SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
1755
+ AGENT_SDK_VERSION = "0.1.15";
1756
+ AGENT_SDK_BUNDLE_HASH = "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
1635
1757
  hookRegistered = false;
1636
1758
  }));
1637
1759
  //#endregion
@@ -1652,7 +1774,7 @@ var init_workflow_loader = __esmMin((() => {
1652
1774
  async function packKiciSource(workDir) {
1653
1775
  const kiciDir = join(workDir, ".kici");
1654
1776
  if (!existsSync(kiciDir)) throw new Error(`.kici/ not found at ${kiciDir}`);
1655
- logger$9.info("Packing .kici/ source tarball", { dir: workDir });
1777
+ logger$10.info("Packing .kici/ source tarball", { dir: workDir });
1656
1778
  const startTime = Date.now();
1657
1779
  const stream = c({
1658
1780
  gzip: true,
@@ -1666,7 +1788,7 @@ async function packKiciSource(workDir) {
1666
1788
  const hash = sha256(tarball);
1667
1789
  const sizeKB = (tarball.length / 1024).toFixed(2);
1668
1790
  const durationMs = Date.now() - startTime;
1669
- logger$9.info(".kici/ source packed", {
1791
+ logger$10.info(".kici/ source packed", {
1670
1792
  sizeKB,
1671
1793
  hash: hash.slice(0, 12),
1672
1794
  durationMs
@@ -1676,9 +1798,9 @@ async function packKiciSource(workDir) {
1676
1798
  hash
1677
1799
  };
1678
1800
  }
1679
- var logger$9;
1801
+ var logger$10;
1680
1802
  var init_source_packer = __esmMin((() => {
1681
- logger$9 = createLogger({ prefix: "source-packer" });
1803
+ logger$10 = createLogger({ prefix: "source-packer" });
1682
1804
  }));
1683
1805
  //#endregion
1684
1806
  //#region src/execution/dep-restore.ts
@@ -1819,7 +1941,7 @@ async function cleanupScratch(scratchDir) {
1819
1941
  force: true
1820
1942
  });
1821
1943
  } catch (cleanupErr) {
1822
- logger$8.warn("Scratch dir cleanup failed (orphan left behind)", {
1944
+ logger$9.warn("Scratch dir cleanup failed (orphan left behind)", {
1823
1945
  scratchDir,
1824
1946
  error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
1825
1947
  });
@@ -1844,7 +1966,7 @@ async function cleanupScratch(scratchDir) {
1844
1966
  */
1845
1967
  async function restoreDeps(workDir, depsUrl, depsHash) {
1846
1968
  depsUrl = resolveOrchestratorUrl(depsUrl);
1847
- logger$8.info("Downloading dependency tarball", { url: depsUrl });
1969
+ logger$9.info("Downloading dependency tarball", { url: depsUrl });
1848
1970
  const kiciDir = join(workDir, ".kici");
1849
1971
  if (depsUrl.startsWith("file://")) {
1850
1972
  const localPath = fileURLToPath(depsUrl);
@@ -1858,7 +1980,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1858
1980
  await moveScratchIntoRepo(scratchDir, workDir);
1859
1981
  await cleanupScratch(scratchDir);
1860
1982
  const sizeMB = (data.length / (1024 * 1024)).toFixed(2);
1861
- logger$8.info("Dependencies restored from cache (file)", {
1983
+ logger$9.info("Dependencies restored from cache (file)", {
1862
1984
  sizeMB,
1863
1985
  targetDir: workDir
1864
1986
  });
@@ -1867,7 +1989,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1867
1989
  if (!depsUrl.startsWith("http://") && !depsUrl.startsWith("https://")) throw new Error(`Unsupported deps URL scheme: ${depsUrl}`);
1868
1990
  let lastError;
1869
1991
  for (let attempt = 0; attempt <= 2; attempt++) {
1870
- if (attempt > 0) logger$8.warn("Retrying dep tarball download", {
1992
+ if (attempt > 0) logger$9.warn("Retrying dep tarball download", {
1871
1993
  attempt,
1872
1994
  url: depsUrl
1873
1995
  });
@@ -1876,11 +1998,11 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1876
1998
  if (depsHash && hash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${hash}`);
1877
1999
  await moveScratchIntoRepo(scratchDir, workDir);
1878
2000
  await cleanupScratch(scratchDir);
1879
- logger$8.info("Dependencies restored from cache (stream)", { targetDir: workDir });
2001
+ logger$9.info("Dependencies restored from cache (stream)", { targetDir: workDir });
1880
2002
  return;
1881
2003
  } catch (err) {
1882
2004
  lastError = err instanceof Error ? err : new Error(String(err));
1883
- logger$8.warn("Dep tarball download failed", {
2005
+ logger$9.warn("Dep tarball download failed", {
1884
2006
  attempt,
1885
2007
  error: lastError.message
1886
2008
  });
@@ -1888,9 +2010,9 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1888
2010
  }
1889
2011
  throw new Error(`Dep tarball download failed after 3 attempts: ${lastError?.message}`);
1890
2012
  }
1891
- var logger$8, DOWNLOAD_TIMEOUT_MS$1, SCRATCH_DIR_BASENAME_PREFIX;
2013
+ var logger$9, DOWNLOAD_TIMEOUT_MS$1, SCRATCH_DIR_BASENAME_PREFIX;
1892
2014
  var init_dep_restore = __esmMin((() => {
1893
- logger$8 = createLogger({ prefix: "dep-restore" });
2015
+ logger$9 = createLogger({ prefix: "dep-restore" });
1894
2016
  DOWNLOAD_TIMEOUT_MS$1 = 300 * 1e3;
1895
2017
  SCRATCH_DIR_BASENAME_PREFIX = ".dep-restore-scratch-";
1896
2018
  `${SCRATCH_DIR_BASENAME_PREFIX}`;
@@ -2002,7 +2124,7 @@ async function extractSourceTarball(data, targetDir) {
2002
2124
  }
2003
2125
  async function restoreSource(workDir, sourceTarUrl) {
2004
2126
  sourceTarUrl = resolveOrchestratorUrl(sourceTarUrl);
2005
- logger$7.info("Restoring .kici/ source from tarball", { sourceTarUrl });
2127
+ logger$8.info("Restoring .kici/ source from tarball", { sourceTarUrl });
2006
2128
  const startTime = Date.now();
2007
2129
  let data;
2008
2130
  if (sourceTarUrl.startsWith("file://")) {
@@ -2012,16 +2134,16 @@ async function restoreSource(workDir, sourceTarUrl) {
2012
2134
  else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
2013
2135
  await extractSourceTarball(data, workDir);
2014
2136
  const durationMs = Date.now() - startTime;
2015
- logger$7.info(".kici/ source restored", {
2137
+ logger$8.info(".kici/ source restored", {
2016
2138
  sizeKB: (data.length / 1024).toFixed(2),
2017
2139
  durationMs
2018
2140
  });
2019
2141
  }
2020
- var logger$7;
2142
+ var logger$8;
2021
2143
  var init_source_restore = __esmMin((() => {
2022
2144
  init_download();
2023
2145
  init_dep_restore();
2024
- logger$7 = createLogger({ prefix: "source-restore" });
2146
+ logger$8 = createLogger({ prefix: "source-restore" });
2025
2147
  }));
2026
2148
  //#endregion
2027
2149
  //#region src/execution/timeout-util.ts
@@ -2069,7 +2191,7 @@ function findJobByName(workflow, jobName) {
2069
2191
  *
2070
2192
  * @param workflow - The extracted Workflow object
2071
2193
  * @param jobName - Name of the job whose dynamic fields to evaluate
2072
- * @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.
2073
2195
  * @param flags - Which fields are dynamic and need evaluation
2074
2196
  * @param timeoutMs - Timeout per dynamic function call (default 60_000ms)
2075
2197
  */
@@ -2258,7 +2380,12 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2258
2380
  log: ctx.log,
2259
2381
  env: ctx.env
2260
2382
  };
2261
- 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
+ }
2262
2389
  if (Array.isArray(values)) return {
2263
2390
  _type: "static",
2264
2391
  values
@@ -2267,11 +2394,19 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2267
2394
  _type: "static",
2268
2395
  values
2269
2396
  };
2270
- 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})`);
2271
2398
  }
2272
- var DYNAMIC_FIELD_TIMEOUT_MS;
2399
+ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
2273
2400
  var init_dynamic_job_serializer = __esmMin((() => {
2274
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
+ };
2275
2410
  DYNAMIC_FIELD_TIMEOUT_MS = 6e4;
2276
2411
  })), DEFAULT_MAX_LOG_SIZE_BYTES, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_FLUSH_LINE_THRESHOLD, PAUSE_SAFETY_TIMEOUT_MS, LogStreamer;
2277
2412
  var init_log_streamer = __esmMin((() => {
@@ -2540,7 +2675,7 @@ async function applyOverlay(config) {
2540
2675
  const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
2541
2676
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2542
2677
  try {
2543
- logger$6.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2678
+ logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2544
2679
  let encryptedData;
2545
2680
  try {
2546
2681
  encryptedData = await downloadUrl(tarballUrl);
@@ -2550,7 +2685,7 @@ async function applyOverlay(config) {
2550
2685
  const cliPubKeyBuf = Buffer.from(cliPublicKey, "base64");
2551
2686
  const aesKey = deriveSharedSecret(Buffer.from(orchestratorPrivateKey, "base64"), cliPubKeyBuf);
2552
2687
  const decryptedData = decryptBuffer(encryptedData, aesKey);
2553
- logger$6.info("Extracting overlay tarball", { size: decryptedData.length });
2688
+ logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
2554
2689
  const extractDir = path.join(tmpDir, "extracted");
2555
2690
  await fs.mkdir(extractDir, { recursive: true });
2556
2691
  try {
@@ -2599,10 +2734,10 @@ async function applyOverlay(config) {
2599
2734
  await fs.unlink(targetPath);
2600
2735
  filesDeleted++;
2601
2736
  } catch {
2602
- logger$6.debug("Deletion target not found, skipping", { file });
2737
+ logger$7.debug("Deletion target not found, skipping", { file });
2603
2738
  }
2604
2739
  }
2605
- logger$6.info("Overlay applied successfully", {
2740
+ logger$7.info("Overlay applied successfully", {
2606
2741
  filesApplied,
2607
2742
  filesDeleted
2608
2743
  });
@@ -2618,10 +2753,10 @@ async function applyOverlay(config) {
2618
2753
  }).catch(() => {});
2619
2754
  }
2620
2755
  }
2621
- var logger$6, IV_LENGTH$1, AUTH_TAG_LENGTH;
2756
+ var logger$7, IV_LENGTH$1, AUTH_TAG_LENGTH;
2622
2757
  var init_overlay_applier = __esmMin((() => {
2623
2758
  init_download();
2624
- logger$6 = createLogger({ prefix: "overlay-applier" });
2759
+ logger$7 = createLogger({ prefix: "overlay-applier" });
2625
2760
  IV_LENGTH$1 = 12;
2626
2761
  AUTH_TAG_LENGTH = 16;
2627
2762
  }));
@@ -2935,7 +3070,7 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
2935
3070
  async function installDeps(kiciDir, opts = {}) {
2936
3071
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
2937
3072
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
2938
- logger$5.info("Installing deps inline", {
3073
+ logger$6.info("Installing deps inline", {
2939
3074
  packageManager,
2940
3075
  dir: kiciDir
2941
3076
  });
@@ -2976,7 +3111,7 @@ async function installDeps(kiciDir, opts = {}) {
2976
3111
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
2977
3112
  const durationMs = Date.now() - startTime;
2978
3113
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
2979
- logger$5.info("Deps installed inline", {
3114
+ logger$6.info("Deps installed inline", {
2980
3115
  packageManager,
2981
3116
  durationMs
2982
3117
  });
@@ -3114,12 +3249,11 @@ function logSubprocessStreams(e, tokens) {
3114
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`);
3115
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`);
3116
3251
  }
3117
- var logger$5, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3252
+ var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3118
3253
  var init_dep_installer = __esmMin((() => {
3119
- init_npm_resolver();
3120
3254
  init_npm_registry_config();
3121
3255
  init_validate_kici_deps();
3122
- logger$5 = createLogger({ prefix: "dep-installer" });
3256
+ logger$6 = createLogger({ prefix: "dep-installer" });
3123
3257
  execFileAsync = promisify(execFile);
3124
3258
  INSTALL_TIMEOUT_MS = 6e5;
3125
3259
  INSTALL_MAX_BUFFER = 128 * 1024 * 1024;
@@ -3158,7 +3292,7 @@ async function packNodeModules(kiciDir) {
3158
3292
  const workDir = dirname(kiciDir);
3159
3293
  const packageManager = await detectPackageManagerFromManifests(workDir) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3160
3294
  const entries = await closureEntries(workDir, kiciDir, packageManager);
3161
- logger$4.info("Packing dependency closure into tarball", {
3295
+ logger$5.info("Packing dependency closure into tarball", {
3162
3296
  dir: workDir,
3163
3297
  packageManager,
3164
3298
  entries
@@ -3174,7 +3308,7 @@ async function packNodeModules(kiciDir) {
3174
3308
  const tarball = Buffer.concat(chunks);
3175
3309
  const hash = sha256(tarball);
3176
3310
  const sizeMB = (tarball.length / (1024 * 1024)).toFixed(2);
3177
- logger$4.info("Dependency closure packed", {
3311
+ logger$5.info("Dependency closure packed", {
3178
3312
  sizeMB,
3179
3313
  hash: hash.slice(0, 12),
3180
3314
  durationMs: Date.now() - startTime
@@ -3262,9 +3396,9 @@ function isInside(root, target) {
3262
3396
  function isAbsoluteRel(rel) {
3263
3397
  return rel.length > 1 && rel[1] === ":";
3264
3398
  }
3265
- var logger$4;
3399
+ var logger$5;
3266
3400
  var init_dep_packer = __esmMin((() => {
3267
- logger$4 = createLogger({ prefix: "dep-packer" });
3401
+ logger$5 = createLogger({ prefix: "dep-packer" });
3268
3402
  }));
3269
3403
  //#endregion
3270
3404
  //#region src/execution/sandbox/env-sanitizer.ts
@@ -3427,6 +3561,7 @@ function buildRequest(dispatch, workDir) {
3427
3561
  contentHash: jobConfig.contentHash,
3428
3562
  resolvedHashFiles: jobConfig.resolvedHashFiles,
3429
3563
  maxLogSizeBytes: dispatch.maxLogSizeBytes,
3564
+ jobTimeoutMs: jobConfig.timeout,
3430
3565
  container: jobConfig.container,
3431
3566
  event: jobConfig.event,
3432
3567
  provider: jobConfig.provider,
@@ -3668,6 +3803,24 @@ function relayAgentApiRequest(msg, ctx) {
3668
3803
  error: toErrorMessage(err)
3669
3804
  }));
3670
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
+ }
3671
3824
  /** Resolve the result promise for `job.complete` IPC messages. Encrypts
3672
3825
  * secret outputs (when a runPublicKey is available) and overrides status to
3673
3826
  * `cancelled` if a cancel was already in flight. */
@@ -3714,7 +3867,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
3714
3867
  ctx.execOptions.onStepStatus(msg.stepIndex, ctx.stepNames.get(msg.stepIndex) ?? "", msg.status, {
3715
3868
  durationMs: msg.durationMs,
3716
3869
  ...msg.error && { error: msg.error },
3717
- ...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
3718
3873
  });
3719
3874
  return;
3720
3875
  case "step.secret_mount":
@@ -3735,6 +3890,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
3735
3890
  case "agent.api.request":
3736
3891
  relayAgentApiRequest(msg, ctx);
3737
3892
  return;
3893
+ case "cache.request":
3894
+ relayCacheRequest$1(msg, ctx);
3895
+ return;
3738
3896
  case "job.complete":
3739
3897
  handleJobComplete(msg, dispatch, ctx);
3740
3898
  return;
@@ -3905,10 +4063,10 @@ var init_fork_runner = __esmMin((() => {
3905
4063
  * network access. This mode provides credential isolation only and should
3906
4064
  * be used in trusted environments.
3907
4065
  */
3908
- var logger$3, BareMetalSandbox;
4066
+ var logger$4, BareMetalSandbox;
3909
4067
  var init_bare_metal_sandbox = __esmMin((() => {
3910
4068
  init_fork_runner();
3911
- logger$3 = createLogger({ prefix: "bare-metal-sandbox" });
4069
+ logger$4 = createLogger({ prefix: "bare-metal-sandbox" });
3912
4070
  BareMetalSandbox = class {
3913
4071
  runnerPath;
3914
4072
  useBwrap;
@@ -3935,12 +4093,12 @@ var init_bare_metal_sandbox = __esmMin((() => {
3935
4093
  if (this.useBwrap) try {
3936
4094
  const { execSync } = await import("node:child_process");
3937
4095
  execSync("which bwrap", { stdio: "ignore" });
3938
- if (this.sandboxNetwork === "isolated") logger$3.info("Bubblewrap (bwrap) sandbox enabled with network isolation (--unshare-net)");
3939
- 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)");
3940
4098
  } catch {
3941
4099
  throw new Error("Bubblewrap (bwrap) not found. Install bubblewrap or set sandbox=false. On Debian/Ubuntu: apt install bubblewrap");
3942
4100
  }
3943
- 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.");
3944
4102
  }
3945
4103
  /**
3946
4104
  * Execute a job by forking the workflow runner with sanitized environment.
@@ -4130,6 +4288,32 @@ function relayApiRequest(stream, options, apiMsg) {
4130
4288
  } catch {}
4131
4289
  }
4132
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
+ /**
4133
4317
  * Apply a job.complete message to the mutable runner state: capture status,
4134
4318
  * merge any bulk-reported step results, propagate plain outputs, and encrypt
4135
4319
  * secret outputs if a run public key is available.
@@ -4144,14 +4328,14 @@ function applyJobComplete(msg, stepResults, state, options) {
4144
4328
  if (msg.secretOutputs && options.dispatch.runPublicKey) try {
4145
4329
  state.encryptedSecretOutputs = encryptSecretOutputs(msg.secretOutputs, options.dispatch.runPublicKey);
4146
4330
  } catch (err) {
4147
- logger$2.warn("Failed to encrypt secret outputs", { error: toErrorMessage(err) });
4331
+ logger$3.warn("Failed to encrypt secret outputs", { error: toErrorMessage(err) });
4148
4332
  }
4149
4333
  }
4150
- 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;
4151
4335
  var init_container_sandbox = __esmMin((() => {
4152
4336
  init_fork_runner();
4153
4337
  init_secret_encryption();
4154
- logger$2 = createLogger({ prefix: "container-sandbox" });
4338
+ logger$3 = createLogger({ prefix: "container-sandbox" });
4155
4339
  MAX_STDERR_LINES = 20;
4156
4340
  ABORT_GRACE_MS = 1e4;
4157
4341
  CONTAINER_STOP_TIMEOUT = 10;
@@ -4183,7 +4367,7 @@ var init_container_sandbox = __esmMin((() => {
4183
4367
  async setup(options) {
4184
4368
  this.containerName = `kici-sandbox-${this.jobId}-${Date.now()}`;
4185
4369
  const envArray = Object.entries(this.env).map(([k, v]) => `${k}=${v}`);
4186
- logger$2.info("Creating sandbox container", {
4370
+ logger$3.info("Creating sandbox container", {
4187
4371
  name: this.containerName,
4188
4372
  image: this.image,
4189
4373
  workDir: options.workDir
@@ -4201,7 +4385,7 @@ var init_container_sandbox = __esmMin((() => {
4201
4385
  HostConfig: { Binds: [`${options.workDir}:/workspace`, `${this.runnerPath}:${this.runnerMountPath}:ro`] }
4202
4386
  });
4203
4387
  await this.container.start();
4204
- logger$2.info("Sandbox container started", {
4388
+ logger$3.info("Sandbox container started", {
4205
4389
  name: this.containerName,
4206
4390
  containerId: this.container.id.slice(0, 12)
4207
4391
  });
@@ -4214,7 +4398,7 @@ var init_container_sandbox = __esmMin((() => {
4214
4398
  try {
4215
4399
  outcome = await this.awaitJobCompletion(streamCtx, options);
4216
4400
  } catch (err) {
4217
- logger$2.error("Job execution error", {
4401
+ logger$3.error("Job execution error", {
4218
4402
  error: toErrorMessage(err),
4219
4403
  stderrTail: streamCtx.stderrLines.slice(-5).join("\n")
4220
4404
  });
@@ -4264,7 +4448,7 @@ var init_container_sandbox = __esmMin((() => {
4264
4448
  });
4265
4449
  const abortHandler = () => {
4266
4450
  this.handleAbort().catch((err) => {
4267
- logger$2.warn("Error during abort", { error: toErrorMessage(err) });
4451
+ logger$3.warn("Error during abort", { error: toErrorMessage(err) });
4268
4452
  });
4269
4453
  };
4270
4454
  options.signal.addEventListener("abort", abortHandler, { once: true });
@@ -4301,7 +4485,7 @@ var init_container_sandbox = __esmMin((() => {
4301
4485
  try {
4302
4486
  msg = JSON.parse(line);
4303
4487
  } catch {
4304
- 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) });
4305
4489
  return;
4306
4490
  }
4307
4491
  if (this.dispatchRunnerMessage(msg, stream, options, stepNames, stepResults, state)) resolve({
@@ -4349,7 +4533,9 @@ var init_container_sandbox = __esmMin((() => {
4349
4533
  options.onStepStatus(msg.stepIndex, name, msg.status, {
4350
4534
  durationMs: msg.durationMs,
4351
4535
  ...msg.error && { error: msg.error },
4352
- ...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
4353
4539
  });
4354
4540
  stepResults.push({
4355
4541
  name,
@@ -4381,11 +4567,14 @@ var init_container_sandbox = __esmMin((() => {
4381
4567
  case "agent.api.request":
4382
4568
  relayApiRequest(stream, options, msg);
4383
4569
  return false;
4570
+ case "cache.request":
4571
+ relayCacheRequest(stream, options, msg);
4572
+ return false;
4384
4573
  case "job.complete":
4385
4574
  applyJobComplete(msg, stepResults, state, options);
4386
4575
  return true;
4387
4576
  default:
4388
- 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 });
4389
4578
  return false;
4390
4579
  }
4391
4580
  }
@@ -4412,14 +4601,14 @@ var init_container_sandbox = __esmMin((() => {
4412
4601
  async teardown() {
4413
4602
  if (!this.container) return;
4414
4603
  if (this.keepFailed && this.jobFailed) {
4415
- logger$2.info("Keeping failed container for debugging", {
4604
+ logger$3.info("Keeping failed container for debugging", {
4416
4605
  name: this.containerName,
4417
4606
  containerId: this.container.id.slice(0, 12)
4418
4607
  });
4419
4608
  this.container = null;
4420
4609
  return;
4421
4610
  }
4422
- logger$2.info("Tearing down sandbox container", { name: this.containerName });
4611
+ logger$3.info("Tearing down sandbox container", { name: this.containerName });
4423
4612
  try {
4424
4613
  await this.container.stop({ t: CONTAINER_STOP_TIMEOUT });
4425
4614
  } catch {}
@@ -4450,7 +4639,7 @@ var init_container_sandbox = __esmMin((() => {
4450
4639
  */
4451
4640
  async handleAbort() {
4452
4641
  if (!this.execStream && !this.container) return;
4453
- logger$2.info("Aborting sandbox execution", { name: this.containerName });
4642
+ logger$3.info("Aborting sandbox execution", { name: this.containerName });
4454
4643
  if (this.execStream) try {
4455
4644
  this.execStream.write(JSON.stringify({ type: "abort" }) + "\n");
4456
4645
  } catch {}
@@ -4519,7 +4708,7 @@ function determineExecutionMode(jobConfig, agentConfig) {
4519
4708
  if (agentConfig.scalerManaged) return "firecracker";
4520
4709
  return "bare-metal";
4521
4710
  }
4522
- var logger$1, JobRunner$1;
4711
+ var logger$2, JobRunner$1;
4523
4712
  var init_job_runner = __esmMin((() => {
4524
4713
  init_git_clone();
4525
4714
  init_workflow_loader();
@@ -4536,7 +4725,7 @@ var init_job_runner = __esmMin((() => {
4536
4725
  init_download();
4537
4726
  init_sandbox();
4538
4727
  init_prometheus();
4539
- logger$1 = createLogger({ prefix: "job-runner" });
4728
+ logger$2 = createLogger({ prefix: "job-runner" });
4540
4729
  JobRunner$1 = class {
4541
4730
  send;
4542
4731
  sendDirect;
@@ -4550,6 +4739,7 @@ var init_job_runner = __esmMin((() => {
4550
4739
  _sendRunEvent;
4551
4740
  _sendConcurrencyReport;
4552
4741
  _sendApiRequest;
4742
+ _requestUserCache;
4553
4743
  /** Tracks running jobs for concurrency and cancellation */
4554
4744
  activeJobs = /* @__PURE__ */ new Map();
4555
4745
  /** Active sandbox for the current job (used for abort). */
@@ -4567,6 +4757,7 @@ var init_job_runner = __esmMin((() => {
4567
4757
  this._sendRunEvent = deps.sendRunEvent;
4568
4758
  this._sendConcurrencyReport = deps.sendConcurrencyReport;
4569
4759
  this._sendApiRequest = deps.sendApiRequest;
4760
+ this._requestUserCache = deps.requestUserCache;
4570
4761
  }
4571
4762
  /**
4572
4763
  * Execute a dispatched job through its full lifecycle.
@@ -4634,7 +4825,7 @@ var init_job_runner = __esmMin((() => {
4634
4825
  }
4635
4826
  if (jobConfig.buildOnly === true) {
4636
4827
  if (jobConfig.fullRepo) {
4637
- 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)", {
4638
4829
  jobId,
4639
4830
  runId
4640
4831
  });
@@ -4656,7 +4847,7 @@ var init_job_runner = __esmMin((() => {
4656
4847
  async executeStandardJob(dispatch, workDir, abortController) {
4657
4848
  const { runId, jobId } = dispatch;
4658
4849
  const ctx = getRequestContext();
4659
- 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"}`);
4660
4851
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
4661
4852
  const heartbeatTimer = setInterval(() => {
4662
4853
  this.send({
@@ -4681,7 +4872,7 @@ var init_job_runner = __esmMin((() => {
4681
4872
  if (sandbox) {
4682
4873
  this.emitRunEvent(runId, "agent.teardown", { jobId });
4683
4874
  await sandbox.teardown().catch((err) => {
4684
- logger$1.warn("Sandbox teardown error", { error: toErrorMessage(err) });
4875
+ logger$2.warn("Sandbox teardown error", { error: toErrorMessage(err) });
4685
4876
  });
4686
4877
  }
4687
4878
  }
@@ -4709,7 +4900,7 @@ var init_job_runner = __esmMin((() => {
4709
4900
  environmentVars: typedConfig.environmentVars ?? void 0,
4710
4901
  jobEnv: typedConfig.jobEnv ?? void 0
4711
4902
  });
4712
- logger$1.info("Creating execution sandbox", {
4903
+ logger$2.info("Creating execution sandbox", {
4713
4904
  executionMode,
4714
4905
  jobId,
4715
4906
  runnerPath
@@ -4780,6 +4971,7 @@ var init_job_runner = __esmMin((() => {
4780
4971
  onStepStatus: (stepIndex, stepName, state, data) => {
4781
4972
  let logBytesStreamed;
4782
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);
4783
4975
  this.sendStepStatus(dispatch, stepIndex, stepName, state, data, logBytesStreamed);
4784
4976
  },
4785
4977
  onLogLine: (stepIndex, line) => {
@@ -4804,6 +4996,7 @@ var init_job_runner = __esmMin((() => {
4804
4996
  };
4805
4997
  },
4806
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,
4807
5000
  onSecretMount: (event) => {
4808
5001
  this.emitRunEvent(runId, "step.secret_mount", {
4809
5002
  jobId,
@@ -4837,7 +5030,7 @@ var init_job_runner = __esmMin((() => {
4837
5030
  stepsTotal.add(1, { status: stepResult.status });
4838
5031
  if (stepResult.durationMs > 0) stepDurationSeconds.record(stepResult.durationMs / 1e3);
4839
5032
  }
4840
- 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", {
4841
5034
  durationMs: result.durationMs,
4842
5035
  stepCount: result.stepResults.length,
4843
5036
  steps: result.stepResults.map((r) => `${r.name}:${r.status}`).join(","),
@@ -4866,7 +5059,7 @@ var init_job_runner = __esmMin((() => {
4866
5059
  async handleBuildJob(dispatch, workDir, abortController) {
4867
5060
  const { runId, jobId, jobConfig } = dispatch;
4868
5061
  const buildCtx = getRequestContext();
4869
- 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"}`);
4870
5063
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
4871
5064
  const buildStreamer = this.createStepStreamer(dispatch, 0);
4872
5065
  const buildLog = (msg) => buildStreamer.addLine(msg);
@@ -4955,14 +5148,14 @@ var init_job_runner = __esmMin((() => {
4955
5148
  const cliPublicKey = jobConfig.cliPublicKey;
4956
5149
  const orchestratorPrivateKey = jobConfig.orchestratorPrivateKey;
4957
5150
  if (tarballUrl && cliPublicKey && orchestratorPrivateKey) {
4958
- logger$1.info("Applying overlay tarball for test run", { jobId });
5151
+ logger$2.info("Applying overlay tarball for test run", { jobId });
4959
5152
  const overlayResult = await applyOverlay({
4960
5153
  tarballUrl,
4961
5154
  cliPublicKey,
4962
5155
  orchestratorPrivateKey,
4963
5156
  repoDir: workDir
4964
5157
  });
4965
- logger$1.info("Overlay applied", {
5158
+ logger$2.info("Overlay applied", {
4966
5159
  filesApplied: overlayResult.filesApplied,
4967
5160
  filesDeleted: overlayResult.filesDeleted
4968
5161
  });
@@ -4990,9 +5183,9 @@ var init_job_runner = __esmMin((() => {
4990
5183
  platform: os.platform(),
4991
5184
  arch: os.arch()
4992
5185
  };
4993
- 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 });
4994
5187
  const depUploadUrl = await this.requestUploadUrl(dispatch.jobId, "deps", depKey);
4995
- logger$1.info("Uploading dep tarball to S3", {
5188
+ logger$2.info("Uploading dep tarball to S3", {
4996
5189
  size: tarball.length,
4997
5190
  hash: hash.slice(0, 12)
4998
5191
  });
@@ -5001,7 +5194,7 @@ var init_job_runner = __esmMin((() => {
5001
5194
  ...depKey,
5002
5195
  depsHash: hash
5003
5196
  });
5004
- logger$1.info("Dep tarball upload complete", { lockfileHash: buildConfig.lockfileHash });
5197
+ logger$2.info("Dep tarball upload complete", { lockfileHash: buildConfig.lockfileHash });
5005
5198
  buildLog(`Deps tarball uploaded (${tarball.length} bytes)`);
5006
5199
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running, {
5007
5200
  buildEvent: "deps_packed",
@@ -5027,15 +5220,15 @@ var init_job_runner = __esmMin((() => {
5027
5220
  platform: os.platform(),
5028
5221
  arch: os.arch()
5029
5222
  };
5030
- 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 });
5031
5224
  const sourceUploadUrl = await this.requestUploadUrl(dispatch.jobId, "source", sourceKey);
5032
- logger$1.info("Uploading source tarball to S3", {
5225
+ logger$2.info("Uploading source tarball to S3", {
5033
5226
  size: tarball.length,
5034
5227
  contentHash: buildConfig.contentHash
5035
5228
  });
5036
5229
  await uploadToPresignedUrl(sourceUploadUrl, tarball);
5037
5230
  this.sendUploadComplete(dispatch.jobId, "source", sourceKey);
5038
- logger$1.info("Source tarball upload complete", { contentHash: buildConfig.contentHash });
5231
+ logger$2.info("Source tarball upload complete", { contentHash: buildConfig.contentHash });
5039
5232
  buildLog(`Source tarball packed and uploaded (${tarball.length} bytes, hash: ${buildConfig.contentHash.slice(0, 12)})`);
5040
5233
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running, {
5041
5234
  buildEvent: "source_packed",
@@ -5058,7 +5251,7 @@ var init_job_runner = __esmMin((() => {
5058
5251
  async handleInitJob(dispatch, workDir, abortController) {
5059
5252
  const { runId, jobId, jobConfig } = dispatch;
5060
5253
  const config = jobConfig;
5061
- logger$1.info("Starting init job", {
5254
+ logger$2.info("Starting init job", {
5062
5255
  jobId,
5063
5256
  targetJobName: config.targetJobName,
5064
5257
  workflowName: config.workflowName
@@ -5106,7 +5299,7 @@ var init_job_runner = __esmMin((() => {
5106
5299
  }
5107
5300
  const kiciDir = join(workDir, ".kici");
5108
5301
  const hasPackage = await fileExists(join(kiciDir, "package.json"));
5109
- logger$1.info("Init job: checking deps", {
5302
+ logger$2.info("Init job: checking deps", {
5110
5303
  kiciDir,
5111
5304
  hasPackageJson: hasPackage,
5112
5305
  source: config.source
@@ -5129,7 +5322,7 @@ var init_job_runner = __esmMin((() => {
5129
5322
  dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
5130
5323
  }, config.timeoutMs);
5131
5324
  });
5132
- logger$1.info("Init job completed successfully", {
5325
+ logger$2.info("Init job completed successfully", {
5133
5326
  jobId,
5134
5327
  hasEnvironment: initResult.environmentName !== void 0,
5135
5328
  hasEnv: initResult.env !== void 0,
@@ -5145,7 +5338,7 @@ var init_job_runner = __esmMin((() => {
5145
5338
  });
5146
5339
  } catch (err) {
5147
5340
  const errorMsg = toErrorMessage(err);
5148
- logger$1.error("Init job failed", {
5341
+ logger$2.error("Init job failed", {
5149
5342
  jobId,
5150
5343
  error: errorMsg
5151
5344
  });
@@ -5172,7 +5365,7 @@ var init_job_runner = __esmMin((() => {
5172
5365
  const { runId, jobId, jobConfig } = dispatch;
5173
5366
  const config = jobConfig;
5174
5367
  const timeoutMs = config.timeoutMs ?? 12e4;
5175
- logger$1.info("Starting DynamicJobFn evaluation", {
5368
+ logger$2.info("Starting DynamicJobFn evaluation", {
5176
5369
  jobId,
5177
5370
  workflowName: config.workflowName,
5178
5371
  sourceIndex: config.source.index
@@ -5273,7 +5466,7 @@ var init_job_runner = __esmMin((() => {
5273
5466
  workflowName: config.workflowName
5274
5467
  });
5275
5468
  });
5276
- logger$1.info("DynamicJobFn evaluation completed", {
5469
+ logger$2.info("DynamicJobFn evaluation completed", {
5277
5470
  jobId,
5278
5471
  generatedJobCount: lockJobs.length,
5279
5472
  jobNames: lockJobs.map((j) => j.name)
@@ -5288,7 +5481,7 @@ var init_job_runner = __esmMin((() => {
5288
5481
  });
5289
5482
  } catch (err) {
5290
5483
  const errorMsg = toErrorMessage(err);
5291
- logger$1.error("DynamicJobFn evaluation failed", {
5484
+ logger$2.error("DynamicJobFn evaluation failed", {
5292
5485
  jobId,
5293
5486
  error: errorMsg
5294
5487
  });
@@ -5296,10 +5489,17 @@ var init_job_runner = __esmMin((() => {
5296
5489
  await evalStreamer.flush();
5297
5490
  evalStreamer.destroy();
5298
5491
  this.sendStepStatus(dispatch, 0, "evaluate", ExecutionStepStatus.enum.failed, { error: errorMsg }, evalStreamer.getTotalBytes());
5299
- this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, {
5492
+ const dynamicData = {
5300
5493
  error: errorMsg,
5301
5494
  dynamicFailed: true
5302
- });
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);
5303
5503
  } finally {
5304
5504
  clearInterval(heartbeatTimer);
5305
5505
  }
@@ -5361,6 +5561,31 @@ var init_job_runner = __esmMin((() => {
5361
5561
  this._sendRunEvent(runId, eventType, opts);
5362
5562
  }
5363
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
+ /**
5364
5589
  * Create a LogStreamer for a synthetic step (build, evaluate, etc.).
5365
5590
  */
5366
5591
  createStepStreamer(dispatch, stepIndex) {
@@ -5444,14 +5669,14 @@ var init_job_runner = __esmMin((() => {
5444
5669
  */
5445
5670
  init_console_capture();
5446
5671
  init_npm_resolver();
5447
- const AGENT_VERSION = "0.1.14";
5448
- const BUILD_COMMIT = "0fc7a43c9";
5449
- const SDK_VERSION = "0.1.14";
5450
- const SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
5451
- const SHARED_VERSION = "0.1.14";
5452
- const SHARED_BUNDLE_HASH = "5a44862f00d382dbac676c0bdbf9e3fb3016d1c4667b60ea4ae6cbf8ac8deaf4";
5453
- const ENGINE_VERSION = "0.1.14";
5454
- const ENGINE_BUNDLE_HASH = "8eef210908744b12e19a2b915520298c5dde5abd4a064fa75e87123e237bf932";
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";
5455
5680
  initTelemetry({
5456
5681
  serviceName: "kici-agent",
5457
5682
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -5459,18 +5684,19 @@ initTelemetry({
5459
5684
  const { connectionStatus, jobsActive, jobsTotal } = await Promise.resolve().then(() => (init_prometheus(), prometheus_exports));
5460
5685
  const { JobRunner } = await Promise.resolve().then(() => (init_job_runner(), job_runner_exports));
5461
5686
  setServiceName("agent");
5462
- const logger = createLogger({ prefix: "agent" });
5687
+ const logger$1 = createLogger({ prefix: "agent" });
5463
5688
  installConsoleCapture();
5464
- await guardStartup(logger, async () => {
5689
+ await guardStartup(logger$1, async () => {
5465
5690
  const config = loadConfig();
5466
- logger.info("Agent starting", {
5691
+ logger$1.info("Agent starting", {
5467
5692
  agentId: config.agentId,
5468
5693
  orchestratorUrl: config.orchestratorUrl,
5469
5694
  labels: config.labels,
5470
5695
  roles: config.roles,
5471
5696
  port: config.port
5472
5697
  });
5473
- logger.info("agent.build.info", {
5698
+ gcStaleAgentTmpDirs();
5699
+ logger$1.info("agent.build.info", {
5474
5700
  agentVersion: AGENT_VERSION,
5475
5701
  buildCommit: BUILD_COMMIT,
5476
5702
  sdkVersion: SDK_VERSION,
@@ -5492,7 +5718,7 @@ await guardStartup(logger, async () => {
5492
5718
  if (toolErrors.length > 0) throw new Error("Agent required-tools validation failed:\n" + toolErrors.map((e) => ` - ${e}`).join("\n"));
5493
5719
  if (config.roles === void 0 || config.roles.includes("builder")) {
5494
5720
  const npmVersion = verifyNpmAvailable();
5495
- logger.info("Builder role: npm verified", { npmVersion });
5721
+ logger$1.info("Builder role: npm verified", { npmVersion });
5496
5722
  }
5497
5723
  let isDraining = false;
5498
5724
  let idleShutdownTimer;
@@ -5509,7 +5735,8 @@ await guardStartup(logger, async () => {
5509
5735
  sendJobContext: (runId, jobId, context) => client.sendJobContext(runId, jobId, context),
5510
5736
  sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
5511
5737
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
5512
- sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {})
5738
+ sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
5739
+ requestUserCache: (jobId, request) => client.requestUserCache(jobId, request)
5513
5740
  });
5514
5741
  /** Build and send an agent.status message with dynamic OS metadata. */
5515
5742
  function sendAgentStatus() {
@@ -5533,15 +5760,31 @@ await guardStartup(logger, async () => {
5533
5760
  jobId: dispatch.jobId
5534
5761
  }, () => {
5535
5762
  if (isDraining) {
5536
- 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
+ });
5537
5772
  sendAgentStatus();
5538
5773
  return;
5539
5774
  }
5540
5775
  if (jobRunner.activeJobs.size > 0) {
5541
- logger.warn("Already running a job, cannot accept another", {
5776
+ logger$1.warn("Already running a job, rejecting dispatch", {
5542
5777
  jobId: dispatch.jobId,
5543
5778
  activeJobs: jobRunner.activeJobs.size
5544
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
+ });
5545
5788
  sendAgentStatus();
5546
5789
  return;
5547
5790
  }
@@ -5549,7 +5792,14 @@ await guardStartup(logger, async () => {
5549
5792
  clearTimeout(idleShutdownTimer);
5550
5793
  idleShutdownTimer = void 0;
5551
5794
  }
5552
- 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", {
5553
5803
  jobId: dispatch.jobId,
5554
5804
  runId: dispatch.runId,
5555
5805
  activeJobs: jobRunner.activeJobs.size + 1
@@ -5558,7 +5808,7 @@ await guardStartup(logger, async () => {
5558
5808
  jobRunner.execute(dispatch).then(() => {
5559
5809
  jobsTotal.add(1, { status: "success" });
5560
5810
  }).catch((err) => {
5561
- logger.error("Job execution error", {
5811
+ logger$1.error("Job execution error", {
5562
5812
  jobId: dispatch.jobId,
5563
5813
  error: toErrorMessage(err)
5564
5814
  });
@@ -5569,7 +5819,7 @@ await guardStartup(logger, async () => {
5569
5819
  sendAgentStatus();
5570
5820
  if (config.scalerManaged && jobRunner.activeJobs.size === 0) {
5571
5821
  if (client.state !== "registered") {
5572
- 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");
5573
5823
  return;
5574
5824
  }
5575
5825
  startIdleShutdownTimer();
@@ -5578,7 +5828,7 @@ await guardStartup(logger, async () => {
5578
5828
  });
5579
5829
  },
5580
5830
  onJobCancel: (cancel) => {
5581
- logger.info("Job cancel received", {
5831
+ logger$1.info("Job cancel received", {
5582
5832
  jobId: cancel.jobId,
5583
5833
  reason: cancel.reason
5584
5834
  });
@@ -5596,13 +5846,13 @@ await guardStartup(logger, async () => {
5596
5846
  const idleMs = config.scalerIdleTimeoutMs;
5597
5847
  if (idleShutdownTimer) clearTimeout(idleShutdownTimer);
5598
5848
  if (idleMs <= 0) {
5599
- logger.info("Scaler-managed agent idle after job completion, shutting down");
5849
+ logger$1.info("Scaler-managed agent idle after job completion, shutting down");
5600
5850
  gracefulShutdown("scaler-idle");
5601
5851
  } else {
5602
- 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`);
5603
5853
  idleShutdownTimer = setTimeout(() => {
5604
5854
  if (jobRunner.activeJobs.size === 0) {
5605
- logger.info("Scaler-managed agent still idle after timeout, shutting down");
5855
+ logger$1.info("Scaler-managed agent still idle after timeout, shutting down");
5606
5856
  gracefulShutdown("scaler-idle");
5607
5857
  }
5608
5858
  }, idleMs);
@@ -5612,17 +5862,17 @@ await guardStartup(logger, async () => {
5612
5862
  if (!config.scalerManaged || jobRunner.activeJobs.size > 0) return;
5613
5863
  if (pendingDispatch) {
5614
5864
  const safetyMs = config.scalerPendingDispatchTimeoutMs;
5615
- 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`);
5616
5866
  if (idleShutdownTimer) clearTimeout(idleShutdownTimer);
5617
5867
  idleShutdownTimer = setTimeout(() => {
5618
5868
  if (jobRunner.activeJobs.size === 0) {
5619
- 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");
5620
5870
  gracefulShutdown("scaler-pending-dispatch-timeout");
5621
5871
  }
5622
5872
  }, safetyMs);
5623
5873
  return;
5624
5874
  }
5625
- 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");
5626
5876
  startIdleShutdownTimer();
5627
5877
  };
5628
5878
  const wsTransport = new winston.transports.Stream({
@@ -5632,14 +5882,14 @@ await guardStartup(logger, async () => {
5632
5882
  } }),
5633
5883
  format: winston.format.combine(winston.format.timestamp(), winston.format.json())
5634
5884
  });
5635
- logger.add(wsTransport);
5885
+ logger$1.add(wsTransport);
5636
5886
  const envProbes = {};
5637
5887
  for (const [k, v] of Object.entries(process.env)) {
5638
5888
  if (v === void 0) continue;
5639
5889
  if (!/_ENV_PROBE$|_ENV_PROBE_/.test(k)) continue;
5640
5890
  envProbes[k] = v.length <= 64 ? v : `${v.slice(0, 61)}...`;
5641
5891
  }
5642
- 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);
5643
5893
  client.connect();
5644
5894
  connectionStatus.add(1);
5645
5895
  const metricsReporter = new MetricsReporter({
@@ -5679,13 +5929,13 @@ await guardStartup(logger, async () => {
5679
5929
  fetch: app.fetch,
5680
5930
  port: config.port
5681
5931
  }, (info) => {
5682
- logger.info(`Agent started on port ${info.port}`, {
5932
+ logger$1.info(`Agent started on port ${info.port}`, {
5683
5933
  port: info.port,
5684
5934
  agentId: config.agentId
5685
5935
  });
5686
5936
  });
5687
5937
  const { shutdown: gracefulShutdown } = setupGracefulShutdown({
5688
- logger,
5938
+ logger: logger$1,
5689
5939
  timeoutMs: 1e4,
5690
5940
  onForceExit: () => {
5691
5941
  for (const job of jobRunner.activeJobs.values()) job.abortController.abort();
@@ -5703,7 +5953,7 @@ await guardStartup(logger, async () => {
5703
5953
  name: "Waiting for active jobs to complete",
5704
5954
  fn: async () => {
5705
5955
  if (jobRunner.activeJobs.size > 0) {
5706
- logger.info("Active jobs remaining", { activeJobs: jobRunner.activeJobs.size });
5956
+ logger$1.info("Active jobs remaining", { activeJobs: jobRunner.activeJobs.size });
5707
5957
  await Promise.allSettled([...jobRunner.activeJobs.values()].map((j) => j.completionPromise));
5708
5958
  }
5709
5959
  }
@@ -5737,17 +5987,17 @@ await guardStartup(logger, async () => {
5737
5987
  ]
5738
5988
  });
5739
5989
  process.on("SIGUSR1", () => {
5740
- logger.info("Received SIGUSR1, entering drain mode");
5990
+ logger$1.info("Received SIGUSR1, entering drain mode");
5741
5991
  isDraining = true;
5742
5992
  if (jobRunner.activeJobs.size === 0) {
5743
- logger.info("No active jobs, shutting down immediately");
5993
+ logger$1.info("No active jobs, shutting down immediately");
5744
5994
  gracefulShutdown("SIGUSR1-drain");
5745
5995
  return;
5746
5996
  }
5747
5997
  const checkDrained = setInterval(() => {
5748
5998
  if (jobRunner.activeJobs.size === 0) {
5749
5999
  clearInterval(checkDrained);
5750
- logger.info("All jobs drained, shutting down");
6000
+ logger$1.info("All jobs drained, shutting down");
5751
6001
  gracefulShutdown("SIGUSR1-drain");
5752
6002
  }
5753
6003
  }, 1e3);