@kikimoradev/kcloud 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +348 -123
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -844,7 +844,6 @@ const errorCodes = Object.keys({
844
844
  "backup.unavailable": "rule_violation",
845
845
  "backup.corrupt": "rule_violation",
846
846
  "backup.undecryptable": "rule_violation",
847
- "volume.invalid_size": "rule_violation",
848
847
  "notification.not_found": "not_found",
849
848
  "logo.not_found": "not_found",
850
849
  "logo.unreadable": "rule_violation",
@@ -1101,7 +1100,8 @@ agentEventSchema.options.map((option) => option.shape.event.value);
1101
1100
  crashLooping: "platform.crashLooping",
1102
1101
  provisioned: "platform.provisioned",
1103
1102
  sandboxStarted: "platform.sandboxStarted",
1104
- sandboxStopped: "platform.sandboxStopped"
1103
+ sandboxStopped: "platform.sandboxStopped",
1104
+ restartedByNode: "platform.restartedByNode"
1105
1105
  }).rawLog;
1106
1106
  const agentStateSchema = z.enum([
1107
1107
  "created",
@@ -1198,41 +1198,11 @@ const degradedAgentSchema = z.object({
1198
1198
  z.object({
1199
1199
  status: z.enum(runtimeHealthStates),
1200
1200
  database: z.object({ ok: z.boolean() }),
1201
- engine: z.enum(runtimeEngineKinds),
1202
- docker: z.object({
1203
- state: z.enum(dockerStates),
1204
- lastError: z.string().nullable(),
1205
- checkedAt: z.iso.datetime().nullable()
1206
- }),
1207
- storage: z.object({
1208
- data: storageStateSchema,
1209
- engine: storageStateSchema
1210
- }),
1211
1201
  partitions: z.object({
1212
1202
  defaultRows: z.int().nonnegative(),
1213
1203
  ensuredThrough: z.string().nullable(),
1214
1204
  lastMaintenanceAt: z.iso.datetime().nullable()
1215
1205
  }),
1216
- reconcile: z.object({
1217
- lastTickAt: z.iso.datetime().nullable(),
1218
- lastTickError: z.string().nullable(),
1219
- agentsQueued: z.int().nonnegative(),
1220
- agentsActive: z.int().nonnegative()
1221
- }),
1222
- ingest: z.object({
1223
- readers: z.int().nonnegative(),
1224
- oldestCursorLagSeconds: z.number().nonnegative().nullable()
1225
- }),
1226
- sandbox: z.object({
1227
- enabled: z.boolean(),
1228
- image: z.object({
1229
- tag: z.string(),
1230
- digest: z.string()
1231
- }),
1232
- agentsWithSandbox: z.int().nonnegative(),
1233
- sidecarsRunning: z.int().nonnegative(),
1234
- volumeBytes: z.int().nonnegative().nullable()
1235
- }),
1236
1206
  metrics: runtimeMetricsSchema,
1237
1207
  deadLetters: z.array(deadLetterQueueSchema),
1238
1208
  degradedAgents: z.array(degradedAgentSchema)
@@ -1706,7 +1676,8 @@ const poolUsageSchema = z.object({
1706
1676
  memoryBytes: z.int().nonnegative(),
1707
1677
  memoryMaxBytes: z.int().nonnegative().nullable(),
1708
1678
  cpus: z.number().nonnegative().nullable(),
1709
- memoryKills: z.int().nonnegative()
1679
+ memoryKills: z.int().nonnegative(),
1680
+ processors: z.int().positive().nullable().optional()
1710
1681
  }).meta({ id: "PoolUsage" });
1711
1682
  const nodePoolSchema = z.object({
1712
1683
  slots: z.int().nonnegative(),
@@ -1715,8 +1686,26 @@ const nodePoolSchema = z.object({
1715
1686
  holders: z.array(sessionHolderSchema).nullable(),
1716
1687
  usage: poolUsageSchema.nullable()
1717
1688
  }).meta({ id: "NodePool" });
1689
+ const sessionRefusalsSchema = z.object({
1690
+ count: z.int().positive(),
1691
+ lastAt: z.iso.datetime(),
1692
+ pool: sessionRoleSchema,
1693
+ reason: z.string()
1694
+ }).meta({ id: "SessionRefusals" });
1695
+ const poolLayoutSchema = z.object({
1696
+ cores: z.array(z.int().positive()).min(1),
1697
+ cpus: z.object({
1698
+ monitor: z.number().positive(),
1699
+ executor: z.number().positive()
1700
+ }).nullable(),
1701
+ topologyError: z.string().nullable(),
1702
+ clamped: z.boolean()
1703
+ }).meta({ id: "PoolLayout" });
1718
1704
  const nodePoolsSchema = z.object({
1719
1705
  enforcement: poolEnforcementSchema,
1706
+ agentsSlice: poolEnforcementSchema.nullable(),
1707
+ refused: sessionRefusalsSchema.nullable(),
1708
+ layout: poolLayoutSchema.nullable(),
1720
1709
  monitor: nodePoolSchema,
1721
1710
  executor: nodePoolSchema
1722
1711
  }).meta({ id: "NodePools" });
@@ -1822,12 +1811,7 @@ const nodeAgentSchema = z.object({
1822
1811
  const nodeRuntimeSchema = z.object({
1823
1812
  reportedAt: z.iso.datetime(),
1824
1813
  docker: z.object({
1825
- state: z.enum([
1826
- "ok",
1827
- "unavailable",
1828
- "disabled",
1829
- "adopting"
1830
- ]),
1814
+ state: z.enum(dockerStates),
1831
1815
  lastError: z.string().nullable()
1832
1816
  }),
1833
1817
  storage: z.object({
@@ -1840,7 +1824,20 @@ const nodeRuntimeSchema = z.object({
1840
1824
  agentsQueued: z.int().nonnegative(),
1841
1825
  agentsActive: z.int().nonnegative()
1842
1826
  }),
1843
- ingest: z.object({ readers: z.int().nonnegative() }),
1827
+ ingest: z.object({
1828
+ readers: z.int().nonnegative(),
1829
+ oldestCursorLagSeconds: z.number().nonnegative().nullable()
1830
+ }),
1831
+ sandbox: z.object({
1832
+ enabled: z.boolean(),
1833
+ image: z.object({
1834
+ tag: z.string(),
1835
+ digest: z.string()
1836
+ }),
1837
+ agentsWithSandbox: z.int().nonnegative(),
1838
+ sidecarsRunning: z.int().nonnegative(),
1839
+ volumeBytes: z.int().nonnegative().nullable()
1840
+ }),
1844
1841
  sessions: z.object({
1845
1842
  monitor: z.object({
1846
1843
  effective: z.int().nonnegative(),
@@ -1859,6 +1856,28 @@ nodeSchema.extend({
1859
1856
  update: nodeUpdateSchema.nullable()
1860
1857
  }).meta({ id: "NodeDetail" });
1861
1858
  z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1859
+ const adminNodeSessionsSchema = z.object({
1860
+ effective: z.int().nonnegative(),
1861
+ inUse: z.int().nonnegative()
1862
+ });
1863
+ const nodeHealthSchema = z.object({
1864
+ docker: z.enum(dockerStates),
1865
+ storage: z.enum(storageStates),
1866
+ reconcileError: z.string().nullable()
1867
+ }).meta({ id: "NodeHealth" });
1868
+ const adminNodeSchema = nodeSchema.extend({
1869
+ organization: z.object({
1870
+ id: idSchema,
1871
+ name: z.string().nullable()
1872
+ }),
1873
+ update: nodeUpdateSchema.nullable(),
1874
+ sessions: z.object({
1875
+ monitor: adminNodeSessionsSchema,
1876
+ executor: adminNodeSessionsSchema
1877
+ }).nullable(),
1878
+ health: nodeHealthSchema.nullable()
1879
+ }).meta({ id: "AdminNode" });
1880
+ z.object({ items: z.array(adminNodeSchema) }).meta({ id: "AdminNodeList" });
1862
1881
  z.object({ nodeId: idSchema });
1863
1882
  z.object({
1864
1883
  organizationId: idSchema,
@@ -1932,6 +1951,30 @@ const splitFits = (split, host) => {
1932
1951
  const overflow = splitOverflowOf(split, host);
1933
1952
  return overflow.cpus <= 0 && overflow.memoryMb <= 0;
1934
1953
  };
1954
+ const poolProcessorsOf = (cpus, hostCpus) => Math.max(1, Math.min(Math.round(cpus), Math.round(hostCpus)));
1955
+ const layoutAppliesSplit = (layout, split) => layout?.cpus != null && split !== null && layout.cpus.monitor === split.monitor.cpus && layout.cpus.executor === split.executor.cpus;
1956
+ const splitCoresOf = (split, cores) => {
1957
+ const processors = cores.reduce((total, size) => total + size, 0);
1958
+ const sizeAt = (index) => cores[Math.min(index, cores.length - 1)] ?? 1;
1959
+ const coresFrom = (first, cpus) => {
1960
+ const wanted = poolProcessorsOf(cpus, processors);
1961
+ let taken = sizeAt(first);
1962
+ let count = 1;
1963
+ while (taken + sizeAt(first + count) <= wanted) {
1964
+ taken += sizeAt(first + count);
1965
+ count += 1;
1966
+ }
1967
+ return count;
1968
+ };
1969
+ const executor = coresFrom(0, split.executor.cpus);
1970
+ const monitor = coresFrom(executor, split.monitor.cpus);
1971
+ return {
1972
+ cores: cores.length,
1973
+ monitor,
1974
+ executor,
1975
+ fits: executor + monitor < cores.length
1976
+ };
1977
+ };
1935
1978
  //#endregion
1936
1979
  //#region ../../packages/contracts/src/appearance.ts
1937
1980
  const colorSchema = z.string().regex(hexColorPattern);
@@ -2571,9 +2614,6 @@ const containerHealthSchema = z.enum([
2571
2614
  ]);
2572
2615
  const maxAgentDescriptionLength = 2e3;
2573
2616
  const maxAgentPromptLength = 64e3;
2574
- const minAgentCpus = .25;
2575
- const maxAgentMemoryMb = 16384;
2576
- const defaultAgentMemoryMb = 4096;
2577
2617
  const workspaceDirectorySchema = z.string().regex(/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/);
2578
2618
  const timeZones = new Set(Intl.supportedValuesOf("timeZone"));
2579
2619
  const timezoneSchema = z.string().trim().refine((value) => timeZones.has(value), { message: "expected an IANA time zone identifier" });
@@ -2584,11 +2624,13 @@ const agentDiskGbSchema = z.int().min(1).max(100);
2584
2624
  const agentMachineSchema = z.object({
2585
2625
  docker: z.boolean(),
2586
2626
  browser: z.boolean(),
2587
- cpus: z.number().min(minAgentCpus).max(8),
2588
- memoryMb: z.int().min(512).max(maxAgentMemoryMb),
2589
2627
  diskGb: agentDiskGbSchema
2590
- }).meta({ id: "AgentMachine" });
2628
+ });
2591
2629
  const agentMachineInputSchema = agentMachineSchema.extend({ diskGb: agentDiskGbSchema.optional() });
2630
+ const agentMachineDtoSchema = agentMachineSchema.extend({
2631
+ cpus: z.number().positive().optional().meta({ deprecated: true }),
2632
+ memoryMb: z.int().positive().optional().meta({ deprecated: true })
2633
+ }).meta({ id: "AgentMachine" });
2592
2634
  const agentPromptsSchema = z.object({
2593
2635
  monitor: agentPromptSchema,
2594
2636
  executor: agentPromptSchema
@@ -2674,7 +2716,7 @@ agentListItemSchema.extend({
2674
2716
  version: z.int().nonnegative(),
2675
2717
  description: z.string(),
2676
2718
  timezone: z.string(),
2677
- machine: agentMachineSchema,
2719
+ machine: agentMachineDtoSchema,
2678
2720
  settings: kikimoraSettingsSchema,
2679
2721
  effectiveSettings: workerSettingsSchema,
2680
2722
  prompts: agentPromptsSchema,
@@ -2816,13 +2858,20 @@ z.object({
2816
2858
  nodeId: idSchema
2817
2859
  }).meta({ id: "RestoreAgentBody" });
2818
2860
  z.object({ agentId: idSchema }).meta({ id: "RestoredAgent" });
2861
+ const agentBackupContentDigestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
2819
2862
  z.object({
2820
2863
  agentId: idSchema,
2821
2864
  trigger: z.enum(agentBackupTriggers),
2822
2865
  kikimoraVersion: z.string().min(1).max(64),
2823
- entries: z.coerce.number().int().nonnegative()
2866
+ entries: z.coerce.number().int().nonnegative(),
2867
+ contentDigest: agentBackupContentDigestSchema.optional()
2824
2868
  });
2825
2869
  z.object({ backupId: idSchema });
2870
+ z.object({
2871
+ agentId: idSchema,
2872
+ contentDigest: agentBackupContentDigestSchema
2873
+ });
2874
+ z.object({ backupId: idSchema.nullable() });
2826
2875
  [
2827
2876
  "application/vnd.oci.image.index.v1+json",
2828
2877
  "application/vnd.oci.image.manifest.v1+json",
@@ -2830,21 +2879,12 @@ z.object({ backupId: idSchema });
2830
2879
  "application/vnd.docker.distribution.manifest.v2+json"
2831
2880
  ].join(", ");
2832
2881
  const pinnedPlatformImagePattern = /^[a-z0-9][a-z0-9.-]*(?::[0-9]{1,5})?(?:\/[a-z0-9][a-z0-9._-]*)+@sha256:[0-9a-f]{64}$/;
2833
- //#endregion
2834
- //#region ../../packages/contracts/src/infrastructure.ts
2835
- const volumeRoles = ["data", "agents"];
2836
- const volumeRoleSchema = z.enum(volumeRoles);
2882
+ const volumeRoleSchema = z.enum(["data", "agents"]);
2837
2883
  const filesystemNameSchema = z.enum([
2838
2884
  "root",
2839
2885
  "data",
2840
2886
  "agents"
2841
2887
  ]);
2842
- z.enum([
2843
- "digitalOcean",
2844
- "spaces",
2845
- "ghcr",
2846
- "backup"
2847
- ]);
2848
2888
  const backupKindSchema = z.enum(["scheduled", "manual"]);
2849
2889
  const backupStatusSchema = z.enum([
2850
2890
  "running",
@@ -2888,65 +2928,42 @@ const sha256HexPattern = /^[0-9a-f]{64}$/;
2888
2928
  const platformSecretPattern = /^[\x21-\x7e]+$/;
2889
2929
  const instantSchema$1 = z.iso.datetime();
2890
2930
  const byteCountSchema$1 = z.int().nonnegative();
2891
- const platformSecretSchema = z.string().trim().min(1).max(maxPlatformSecretLength).regex(platformSecretPattern, { message: "expected printable ASCII characters without spaces" });
2931
+ z.string().trim().min(1).max(maxPlatformSecretLength).regex(platformSecretPattern, { message: "expected printable ASCII characters without spaces" });
2892
2932
  const secretPresenceSchema = z.object({ present: z.boolean() }).meta({ id: "SecretPresence" });
2893
2933
  const regionSchema = z.string().regex(regionSlugPattern);
2934
+ const bucketNameSchema = z.string().regex(bucketNamePattern);
2894
2935
  const platformVolumeSchema = z.object({
2895
2936
  role: volumeRoleSchema,
2896
2937
  id: z.guid(),
2897
2938
  name: z.string().regex(volumeNamePattern)
2898
2939
  }).meta({ id: "PlatformVolume" });
2899
- const oneVolumePerRole = (volumes) => volumes.length === volumeRoles.length && volumeRoles.every((role) => volumes.some((volume) => volume.role === role));
2900
- const platformVolumesSchema = z.array(platformVolumeSchema).refine(oneVolumePerRole, { message: `expected exactly one volume for each role: ${volumeRoles.join(", ")}` });
2940
+ const countOfRole = (volumes, role) => volumes.filter((volume) => volume.role === role).length;
2941
+ const oneDataVolumeAndAtMostOneAgentsVolume = (volumes) => countOfRole(volumes, "data") === 1 && countOfRole(volumes, "agents") <= 1;
2942
+ const platformVolumesSchema = z.array(platformVolumeSchema).refine(oneDataVolumeAndAtMostOneAgentsVolume, { message: "expected one data volume and at most one agents volume" });
2901
2943
  const isOrigin = (value) => {
2902
2944
  if (!URL.canParse(value)) return false;
2903
2945
  const url = new URL(value);
2904
2946
  return url.pathname === "/" && url.search === "" && url.hash === "" && url.username === "" && url.password === "";
2905
2947
  };
2906
2948
  const spacesEndpointSchema = z.url({ protocol: /^https?$/ }).max(2e3).refine(isOrigin, { message: "expected an origin without a path, a query, a fragment or credentials" });
2907
- const digitalOceanShape = {
2908
- dropletId: z.int().positive(),
2909
- region: regionSchema,
2910
- reservedIp: z.ipv4(),
2911
- volumes: platformVolumesSchema
2912
- };
2913
- const spacesShape = {
2914
- endpoint: spacesEndpointSchema,
2915
- region: regionSchema,
2916
- bucket: z.string().regex(bucketNamePattern)
2917
- };
2918
2949
  const backupSettingsSchema = z.object({ retentionDays: z.int().min(1).max(maxBackupRetentionDays) }).meta({ id: "BackupSettings" });
2919
2950
  z.object({
2920
- version: z.int().nonnegative(),
2921
2951
  instance: z.string(),
2922
2952
  domain: z.string(),
2923
2953
  digitalOcean: z.object({
2924
- ...digitalOceanShape,
2925
- token: secretPresenceSchema
2954
+ dropletId: z.int().positive(),
2955
+ region: regionSchema,
2956
+ reservedIp: z.ipv4(),
2957
+ volumes: platformVolumesSchema
2926
2958
  }).nullable(),
2927
2959
  spaces: z.object({
2928
- ...spacesShape,
2929
- accessKey: secretPresenceSchema,
2930
- secretKey: secretPresenceSchema
2960
+ endpoint: spacesEndpointSchema,
2961
+ region: regionSchema,
2962
+ bucket: bucketNameSchema
2931
2963
  }).nullable(),
2932
2964
  ghcrToken: secretPresenceSchema,
2933
- backup: backupSettingsSchema,
2934
- updatedAt: instantSchema$1
2965
+ backup: backupSettingsSchema
2935
2966
  }).meta({ id: "PlatformSettings" });
2936
- z.object({
2937
- version: z.int().nonnegative(),
2938
- digitalOcean: z.object({
2939
- ...digitalOceanShape,
2940
- token: platformSecretSchema.nullable()
2941
- }).nullable(),
2942
- spaces: z.object({
2943
- ...spacesShape,
2944
- accessKey: platformSecretSchema.nullable(),
2945
- secretKey: platformSecretSchema.nullable()
2946
- }).nullable(),
2947
- ghcrToken: platformSecretSchema.nullable(),
2948
- backup: backupSettingsSchema.nullable()
2949
- }).meta({ id: "UpdatePlatformSettingsBody" });
2950
2967
  const storageReadingSchema = z.discriminatedUnion("measured", [z.object({
2951
2968
  measured: z.literal(true),
2952
2969
  path: z.string(),
@@ -3010,8 +3027,6 @@ z.object({
3010
3027
  dockerRootDir: z.string().nullable()
3011
3028
  })
3012
3029
  }).meta({ id: "PlatformNode" });
3013
- z.object({ role: volumeRoleSchema });
3014
- z.object({ sizeGb: z.int().positive() }).meta({ id: "ResizeVolumeBody" });
3015
3030
  const requesterSchema = z.object({
3016
3031
  userId: idSchema,
3017
3032
  label: z.string().nullable()
@@ -3148,12 +3163,15 @@ z.object({
3148
3163
  versions: z.array(masterKeyVersionSchema),
3149
3164
  rotationPending: z.boolean()
3150
3165
  }).meta({ id: "MasterKey" });
3151
- const metricsRangeSchema = z.enum([
3166
+ //#endregion
3167
+ //#region ../../packages/contracts/src/metrics.ts
3168
+ const metricsRanges = [
3152
3169
  "1h",
3153
3170
  "24h",
3154
3171
  "7d",
3155
3172
  "90d"
3156
- ]);
3173
+ ];
3174
+ const metricsRangeSchema = z.enum(metricsRanges);
3157
3175
  const agentMetricsRanges = [
3158
3176
  "1h",
3159
3177
  "24h",
@@ -3207,18 +3225,13 @@ const agentUsageSchema = z.object({
3207
3225
  blockReadBytes: byteCountSchema,
3208
3226
  blockWriteBytes: byteCountSchema
3209
3227
  }).meta({ id: "AgentUsage" });
3210
- const nodeHostSeriesSchema = z.object({
3211
- nodeId: idSchema,
3212
- nodeName: z.string(),
3213
- points: z.array(hostPointSchema)
3214
- }).meta({ id: "NodeHostSeries" });
3215
3228
  z.object({
3216
3229
  resolution: metricsResolutionSchema,
3217
3230
  from: instantSchema,
3218
3231
  to: instantSchema,
3219
- nodes: z.array(nodeHostSeriesSchema),
3232
+ points: z.array(hostPointSchema),
3220
3233
  agents: z.array(agentUsageSchema)
3221
- }).meta({ id: "HostMetrics" });
3234
+ }).meta({ id: "NodeMetrics" });
3222
3235
  z.object({
3223
3236
  resolution: metricsResolutionSchema,
3224
3237
  from: instantSchema,
@@ -3255,7 +3268,8 @@ const notificationTargetTypeSchema = z.enum([
3255
3268
  "backup",
3256
3269
  "queue",
3257
3270
  "volume",
3258
- "platform"
3271
+ "platform",
3272
+ "node"
3259
3273
  ]);
3260
3274
  const hostOperationOutcomes = [
3261
3275
  "succeeded",
@@ -4160,6 +4174,13 @@ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe
4160
4174
  organizationId,
4161
4175
  nodeId
4162
4176
  } } }), nodeHints);
4177
+ const nodeMetricsOf = async (api, { organizationId, nodeId }, range) => dataOf("show what the node used", await api.GET("/organizations/{organizationId}/nodes/{nodeId}/metrics", { params: {
4178
+ path: {
4179
+ organizationId,
4180
+ nodeId
4181
+ },
4182
+ query: { range }
4183
+ } }), nodeHints);
4163
4184
  const createdNode = async (api, organizationId, body) => {
4164
4185
  validated(createNodeBodySchema, body, "a node");
4165
4186
  return providerDataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
@@ -4495,6 +4516,23 @@ const enforcementText = ({ state, reason }) => {
4495
4516
  case "unsupported": return `The node holds no pool to a limit: ${reason === null ? "it cannot" : unsupportedReasons[reason] ?? reason}.`;
4496
4517
  }
4497
4518
  };
4519
+ const agentsSliceReasons = {
4520
+ cgroup_driver: "Docker on the host does not use the systemd cgroup driver",
4521
+ cgroup_driver_unread: "the node has not read the cgroup driver of Docker yet",
4522
+ host_unread: "the node has not read its host yet",
4523
+ configure_failed: "systemd did not take the limits",
4524
+ pending: "the node has not applied the limit yet",
4525
+ platform: "the node runs no pool service"
4526
+ };
4527
+ const agentsSliceText = ({ state, reason }) => {
4528
+ switch (state) {
4529
+ case "enforced": return "The node holds its agents and their sessions together to the host minus its reserve.";
4530
+ case "unsplit": return "The node has no split, so its agents outside their sessions have no limit.";
4531
+ case "unsupported": return `The node holds its agents outside their sessions to no limit: ${reason === null ? "it cannot" : agentsSliceReasons[reason] ?? reason}.`;
4532
+ }
4533
+ };
4534
+ const refusedText = ({ count, pool, lastAt, reason }) => `The session starter refused ${count === 1 ? "1 session" : `${count} sessions`} of the ${pool} pool in the last hour, the last at ${lastAt}: ${reason}`;
4535
+ const layoutTexts = (layout, split) => [...layout.topologyError === null ? [] : [`The node did not read its processor cores, so each logical processor counts as a core and a pool can share a physical core: ${layout.topologyError}`], ...layout.clamped && layoutAppliesSplit(layout, split) ? ["The split does not fit the cores of the node, so its pools run on fewer cores than the split asks for; kcloud nodes split <node> shows the cores each pool takes."] : []];
4498
4536
  const memoryOf$1 = (pool) => {
4499
4537
  if (pool.usage === null) return "-";
4500
4538
  const { memoryBytes, memoryMaxBytes } = pool.usage;
@@ -4549,10 +4587,13 @@ const queueOf = (pools) => {
4549
4587
  { header: "WORK" }
4550
4588
  ], waiting);
4551
4589
  };
4552
- const poolsText = (pools) => {
4590
+ const poolsText = (pools, split) => {
4553
4591
  const holders = holdersOf(pools);
4554
4592
  return [
4555
4593
  enforcementText(pools.enforcement),
4594
+ ...pools.agentsSlice == null ? [] : [agentsSliceText(pools.agentsSlice)],
4595
+ ...pools.refused == null ? [] : [refusedText(pools.refused)],
4596
+ ...pools.layout == null ? [] : layoutTexts(pools.layout, split),
4556
4597
  renderTable([
4557
4598
  { header: "POOL" },
4558
4599
  { header: "SLOTS IN USE" },
@@ -4603,8 +4644,36 @@ const agentPlanOf = (agent) => ({
4603
4644
  prompts: agent.prompts,
4604
4645
  settings: agent.settings,
4605
4646
  timezone: agent.timezone,
4606
- machine: agent.machine
4647
+ machine: {
4648
+ docker: agent.machine.docker,
4649
+ browser: agent.machine.browser,
4650
+ diskGb: agent.machine.diskGb
4651
+ }
4607
4652
  });
4653
+ const machineLimitKeys = /* @__PURE__ */ new Set(["cpus", "memoryMb"]);
4654
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4655
+ const warnIgnored = ({ context }, keys) => {
4656
+ const named = keys.map((key) => `machine.${key}`).join(" and ");
4657
+ context.terminal.error(context.terminal.errorStyles.warning(`warning ${named} ${keys.length === 1 ? "is" : "are"} ignored: an agent has no limits of its own, and the pools of its node limit its sessions.`));
4658
+ };
4659
+ const machineWithoutLimits = (run, machine) => {
4660
+ if (!isRecord(machine)) return machine;
4661
+ const ignored = Object.keys(machine).filter((key) => machineLimitKeys.has(key));
4662
+ if (ignored.length === 0) return machine;
4663
+ warnIgnored(run, ignored);
4664
+ return Object.fromEntries(Object.entries(machine).filter(([key]) => !machineLimitKeys.has(key)));
4665
+ };
4666
+ const agentFileWithoutLimits = (run, document) => isRecord(document) && "machine" in document ? {
4667
+ ...document,
4668
+ machine: machineWithoutLimits(run, document.machine)
4669
+ } : document;
4670
+ const agentChangeWithoutLimits = (run, document) => isRecord(document) && isRecord(document.sections) && "machine" in document.sections ? {
4671
+ ...document,
4672
+ sections: {
4673
+ ...document.sections,
4674
+ machine: machineWithoutLimits(run, document.sections.machine)
4675
+ }
4676
+ } : document;
4608
4677
  //#endregion
4609
4678
  //#region src/commands/agents/config.ts
4610
4679
  const agentsOf = async (scope, project) => {
@@ -4715,7 +4784,7 @@ const configCommands = (adaptersOf) => [
4715
4784
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
4716
4785
  const file = invocation.values.file;
4717
4786
  if (file === void 0) throw new UsageError("agents set needs --file <path>.", "The file holds the sections to change; kcloud agents show <agent> --json shows the current ones.");
4718
- const agent = await changedAgent(run.api, agentId, await documentOf(run, file));
4787
+ const agent = await changedAgent(run.api, agentId, agentChangeWithoutLimits(run, await documentOf(run, file)));
4719
4788
  return printed(run, agent, (changed) => changed.restartRequired ? `Changed ${changed.name}; it needs kcloud agents update to take the change.` : `Changed ${changed.name}.`);
4720
4789
  }
4721
4790
  }),
@@ -4781,7 +4850,7 @@ const detailsOf = (agent) => [
4781
4850
  ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
4782
4851
  ["requires", agent.requiredCapabilities.length === 0 ? "nothing" : agent.requiredCapabilities.join(", ")],
4783
4852
  ["timezone", agent.timezone],
4784
- ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
4853
+ ["disk", `${String(agent.machine.diskGb)} GB`],
4785
4854
  ["docker", agent.machine.docker ? "yes" : "no"],
4786
4855
  ["browser", agent.machine.browser ? "yes" : "no"],
4787
4856
  ["container", agent.runtime.containerId ?? "none"],
@@ -4976,15 +5045,13 @@ const askedPlan = async (run, projectId, values, required) => {
4976
5045
  timezone: values.timezone ?? localTimezone(),
4977
5046
  machine: {
4978
5047
  docker: values.docker,
4979
- browser: values.browser,
4980
- cpus: 2,
4981
- memoryMb: defaultAgentMemoryMb
5048
+ browser: values.browser
4982
5049
  }
4983
5050
  };
4984
5051
  };
4985
5052
  const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
4986
5053
  const fileOf = async (run, projectId, { file, node }, required) => {
4987
- const document = await documentOf(run, file);
5054
+ const document = agentFileWithoutLimits(run, await documentOf(run, file));
4988
5055
  const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
4989
5056
  ...document,
4990
5057
  nodeId: await placedNodeId(run, {
@@ -6689,6 +6756,115 @@ const agentTools = [
6689
6756
  run: async ({ action, now, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, action === "stop" && now)
6690
6757
  })
6691
6758
  ];
6759
+ const percentOf = (ratio) => ratio === null ? "-" : `${(ratio * 100).toFixed(1)}%`;
6760
+ const disksOf = (point) => [
6761
+ point.diskRootUsedRatio,
6762
+ point.diskDataUsedRatio,
6763
+ point.diskAgentsUsedRatio
6764
+ ].map(percentOf).join(" / ");
6765
+ const gpuMemoryOf = ({ gpuMemoryUsedBytesAvg: used, gpuMemoryTotalBytes: total }) => used === null || total === null ? "-" : `${bytesOf(used)} of ${bytesOf(total)}`;
6766
+ const gpuOf = (point) => `${percentOf(point.gpuUtilRatioAvg)}, ${gpuMemoryOf(point)}`;
6767
+ const readsAGpu = (points) => points.some((point) => point.gpuUtilRatioAvg !== null || point.gpuMemoryTotalBytes !== null);
6768
+ const hostText$1 = (points) => {
6769
+ const gpu = readsAGpu(points);
6770
+ return renderTable([
6771
+ { header: "AT" },
6772
+ {
6773
+ header: "CPU",
6774
+ align: "right"
6775
+ },
6776
+ {
6777
+ header: "MEMORY",
6778
+ align: "right"
6779
+ },
6780
+ {
6781
+ header: "LOAD",
6782
+ align: "right"
6783
+ },
6784
+ {
6785
+ header: "DISKS ROOT / DATA / AGENTS",
6786
+ align: "right"
6787
+ },
6788
+ ...gpu ? [{
6789
+ header: "GPU",
6790
+ align: "right"
6791
+ }] : []
6792
+ ], points.map((point) => [
6793
+ point.ts,
6794
+ percentOf(point.cpuRatioAvg),
6795
+ `${bytesOf(point.memoryUsedBytesAvg)} of ${bytesOf(point.memoryTotalBytes)}`,
6796
+ point.load1Avg === null ? "-" : point.load1Avg.toFixed(2),
6797
+ disksOf(point),
6798
+ ...gpu ? [gpuOf(point)] : []
6799
+ ]));
6800
+ };
6801
+ const byFootprint = (left, right) => right.memoryBytesAvg - left.memoryBytesAvg || left.agentName.localeCompare(right.agentName);
6802
+ const agentsText = (agents) => renderTable([
6803
+ { header: "PROJECT" },
6804
+ { header: "AGENT" },
6805
+ {
6806
+ header: "CPU (CORES)",
6807
+ align: "right"
6808
+ },
6809
+ {
6810
+ header: "MEMORY AVG",
6811
+ align: "right"
6812
+ },
6813
+ {
6814
+ header: "MEMORY MAX",
6815
+ align: "right"
6816
+ },
6817
+ {
6818
+ header: "NETWORK IN / OUT",
6819
+ align: "right"
6820
+ },
6821
+ {
6822
+ header: "DISK READ / WRITE",
6823
+ align: "right"
6824
+ }
6825
+ ], agents.toSorted(byFootprint).map((usage) => [
6826
+ usage.projectName,
6827
+ usage.agentName,
6828
+ usage.cpuRatioAvg === null ? "-" : usage.cpuRatioAvg.toFixed(2),
6829
+ bytesOf(usage.memoryBytesAvg),
6830
+ bytesOf(usage.memoryBytesMax),
6831
+ `${bytesOf(usage.networkRxBytes)} / ${bytesOf(usage.networkTxBytes)}`,
6832
+ `${bytesOf(usage.blockReadBytes)} / ${bytesOf(usage.blockWriteBytes)}`
6833
+ ]));
6834
+ const nodeMetricsText = (metrics) => [
6835
+ `${metrics.from} to ${metrics.to}, one point per ${metrics.resolution}.`,
6836
+ "",
6837
+ metrics.points.length === 0 ? "The node sent no sample in that range." : hostText$1(metrics.points),
6838
+ "",
6839
+ metrics.agents.length === 0 ? "No agent you can see ran on the node in that range." : agentsText(metrics.agents)
6840
+ ].join("\n");
6841
+ const rangeOf = (given) => {
6842
+ if (given === void 0) return "1h";
6843
+ const range = metricsRanges.find((candidate) => candidate === given);
6844
+ if (range === void 0) throw new UsageError(`--range ${given} is not a range of metrics.`, `The ranges are ${metricsRanges.join(", ")}.`);
6845
+ return range;
6846
+ };
6847
+ const createNodeMetricsCommand = (adaptersOf) => defineCommand({
6848
+ name: "metrics",
6849
+ summary: "Show what the host of a node and the agents on it used over a range.",
6850
+ usage: "nodes metrics <node> [--range <range>]",
6851
+ optionsUsage: [
6852
+ ...nodeUsage,
6853
+ ["--range <range>", `how far back: ${metricsRanges.join(", ")} (default: 1h)`],
6854
+ ["", "the agents are those of the projects you belong to, or all for an admin"]
6855
+ ],
6856
+ options: { range: { type: "string" } },
6857
+ positionals: {
6858
+ min: 1,
6859
+ max: 1
6860
+ },
6861
+ run: async (invocation) => {
6862
+ const range = rangeOf(invocation.values.range);
6863
+ const run = await runOf(adaptersOf, invocation);
6864
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
6865
+ return printed(run, await nodeMetricsOf(run.api, reference, range), nodeMetricsText);
6866
+ }
6867
+ });
6692
6868
  //#endregion
6693
6869
  //#region src/commands/nodes/split.ts
6694
6870
  const splitFields = [
@@ -6744,6 +6920,8 @@ const changeOfParts = ({ reserve, monitor, executor }) => {
6744
6920
  const cpusText = (cpus) => String(Math.round(cpus * 100) / 100);
6745
6921
  const hostText = (host) => `${cpusText(host.cpus)} CPU and ${hostMemoryMbOf(host)} MB`;
6746
6922
  const excessText = ({ cpus, memoryMb }) => [...cpus > 0 ? [`${cpusText(cpus)} CPU`] : [], ...memoryMb > 0 ? [`${memoryMb} MB`] : []].join(" and ");
6923
+ const coresText = (count) => count === 1 ? "1 core" : `${String(count)} cores`;
6924
+ const coresOfNode = (node) => node.runtime?.pools.layout?.cores ?? null;
6747
6925
  const fittingSplit = (node, change, nameOf) => {
6748
6926
  const merged = {
6749
6927
  ...node.split === null ? {} : valuesOf(node.split),
@@ -6757,9 +6935,25 @@ const fittingSplit = (node, change, nameOf) => {
6757
6935
  validated(nodeSplitSchema, split, "a split of a node");
6758
6936
  if (node.host === null) throw new CliError(`${node.name} has not reported its CPUs and memory yet, so it cannot be split.`, "The node reports them once its service runs; kcloud nodes show <node> shows its state.");
6759
6937
  if (!splitFits(split, node.host)) throw new CliError(`The split asks for ${excessText(splitOverflowOf(split, node.host))} more than ${node.name} has.`, `${node.name} has ${hostText(node.host)}; shrink a pool or the reserve.`);
6938
+ const cores = coresOfNode(node);
6939
+ const fit = cores === null ? null : splitCoresOf(split, cores);
6940
+ if (fit !== null && !fit.fits) throw new CliError(`The pools take whole cores of ${node.name}: the executor pool would get ${coresText(fit.executor)} and the monitor pool ${coresText(fit.monitor)} of ${String(fit.cores)}, and the reserve keeps at least 1.`, "Shrink a pool, so the reserve keeps the core of processor 0.");
6760
6941
  return split;
6761
6942
  };
6943
+ const processorCountsOf = (node) => {
6944
+ const pools = node.runtime?.pools;
6945
+ if (pools === void 0 || !layoutAppliesSplit(pools.layout, node.split)) return null;
6946
+ const monitor = pools.monitor.usage?.processors ?? null;
6947
+ const executor = pools.executor.usage?.processors ?? null;
6948
+ return monitor === null && executor === null ? null : {
6949
+ monitor,
6950
+ executor
6951
+ };
6952
+ };
6953
+ const processorsText = (count) => count === 1 ? "1 processor" : `${String(count)} processors`;
6954
+ const poolCpusText = (cpus, processors) => processors === null ? cpusText(cpus) : `${cpusText(cpus)} (${processorsText(processors)})`;
6762
6955
  const splitViewOf = (node) => {
6956
+ const cores = coresOfNode(node);
6763
6957
  const overflow = node.split === null || node.host === null ? null : splitOverflowOf(node.split, node.host);
6764
6958
  return {
6765
6959
  id: node.id,
@@ -6770,6 +6964,8 @@ const splitViewOf = (node) => {
6770
6964
  memoryMb: hostMemoryMbOf(node.host)
6771
6965
  },
6772
6966
  split: node.split,
6967
+ processors: processorCountsOf(node),
6968
+ cores: cores === null || node.split === null ? null : splitCoresOf(node.split, cores),
6773
6969
  free: overflow === null ? null : {
6774
6970
  cpus: -overflow.cpus,
6775
6971
  memoryMb: -overflow.memoryMb
@@ -6779,10 +6975,12 @@ const splitViewOf = (node) => {
6779
6975
  const resourceCells = ({ cpus, memoryMb }) => [cpusText(cpus), `${memoryMb} MB`];
6780
6976
  const oneSlotOf = ({ cpus, memoryMb, slots }) => `${cpusText(cpus / slots)} CPU, ${Math.floor(memoryMb / slots)} MB`;
6781
6977
  const hostLineOf = (view) => view.host === null ? `${view.name} has not reported its CPUs and memory yet.` : `The host has ${cpusText(view.host.cpus)} CPU and ${view.host.memoryMb} MB.`;
6978
+ const coresLineOf = ({ cores, executor, monitor, fits }) => fits ? `The pools take whole cores: the executor pool ${coresText(executor)} and the monitor pool ${coresText(monitor)}; the reserve keeps ${coresText(cores - executor - monitor)} of ${String(cores)}.` : `The executor pool would take ${coresText(executor)} and the monitor pool ${coresText(monitor)} of ${String(cores)}, so the node runs them on fewer cores and keeps 1 for the reserve.`;
6782
6979
  const splitTableOf = (view, split) => {
6783
6980
  const poolRow = (role) => [
6784
6981
  role,
6785
- ...resourceCells(split[role]),
6982
+ poolCpusText(split[role].cpus, view.processors?.[role] ?? null),
6983
+ `${String(split[role].memoryMb)} MB`,
6786
6984
  String(split[role].slots),
6787
6985
  oneSlotOf(split[role])
6788
6986
  ];
@@ -6830,7 +7028,8 @@ const splitTableOf = (view, split) => {
6830
7028
  { header: "ONE SLOT" }
6831
7029
  ], rows),
6832
7030
  ...view.host === null ? ["", hostLineOf(view)] : [],
6833
- ...over === "" ? [] : ["", `The split asks for ${over} more than the host has.`]
7031
+ ...over === "" ? [] : ["", `The split asks for ${over} more than the host has.`],
7032
+ ...view.cores === null ? [] : ["", coresLineOf(view.cores)]
6834
7033
  ].join("\n");
6835
7034
  };
6836
7035
  const splitText = (view) => view.split === null ? [
@@ -6838,10 +7037,14 @@ const splitText = (view) => view.split === null ? [
6838
7037
  hostLineOf(view),
6839
7038
  `kcloud nodes split set ${view.name} with every part splits it.`
6840
7039
  ].join("\n") : splitTableOf(view, view.split);
6841
- const splitSummaryOf = (split) => {
7040
+ const splitSummaryOf = (split, processors = null) => {
6842
7041
  if (split === null) return "none, slots counted from the host without hard limits";
6843
7042
  const { reserve, monitor, executor } = split;
6844
- const pool = (role, { cpus, memoryMb, slots }) => `${role} ${cpusText(cpus)} CPU, ${memoryMb} MB, ${slots} slots`;
7043
+ const pool = (role, { cpus, memoryMb, slots }) => {
7044
+ const count = processors?.[role] ?? null;
7045
+ const onProcessors = count === null ? "" : ` (${processorsText(count)})`;
7046
+ return `${role} ${cpusText(cpus)} CPU${onProcessors}, ${memoryMb} MB, ${slots} slots`;
7047
+ };
6845
7048
  return [
6846
7049
  `reserve ${cpusText(reserve.cpus)} CPU, ${reserve.memoryMb} MB`,
6847
7050
  pool("monitor", monitor),
@@ -7023,6 +7226,18 @@ const nodeTools = [
7023
7226
  input: z.object({ node: nodeField }),
7024
7227
  run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
7025
7228
  }),
7229
+ defineTool({
7230
+ name: "node_metrics",
7231
+ title: "Show what the host of a node and its agents used",
7232
+ description: "Show the CPU, memory, load, disks and GPU of the host of a node over a range, and what each agent on it used over the whole range: the agents of the projects the caller belongs to, or every agent for an admin of the organization.",
7233
+ operations: ["getNodeMetrics"],
7234
+ safety: "read",
7235
+ input: z.object({
7236
+ node: nodeField,
7237
+ range: z.enum(metricsRanges).default("1h")
7238
+ }),
7239
+ run: async ({ node, range }, run) => nodeMetricsOf(run.api, await resolveNode(run, node), range)
7240
+ }),
7026
7241
  defineTool({
7027
7242
  name: "node_split_set",
7028
7243
  title: "Split a node into pools",
@@ -9684,14 +9899,23 @@ const operationRowsOf = (operation) => {
9684
9899
  ...error === null ? [] : [["error", error]]
9685
9900
  ];
9686
9901
  };
9687
- const runtimeRowsOf = (runtime) => [
9902
+ const lagOf = ({ oldestCursorLagSeconds }) => oldestCursorLagSeconds === null ? "-" : `${oldestCursorLagSeconds} s`;
9903
+ const sandboxRowsOf = ({ sandbox }) => sandbox.enabled ? [
9904
+ ["sandboxes", `enabled, image ${sandbox.image.tag}`],
9905
+ ["agents with docker", String(sandbox.agentsWithSandbox)],
9906
+ ["sidecars running", String(sandbox.sidecarsRunning)],
9907
+ ["sandbox volumes", sandbox.volumeBytes === null ? "not measured yet" : bytesOf(sandbox.volumeBytes)]
9908
+ ] : [["sandboxes", "disabled, SANDBOX_ENABLED is off on the node"]];
9909
+ const runtimeRowsOf = (runtime, engine) => [
9688
9910
  ["reported", runtime.reportedAt],
9689
9911
  ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
9690
9912
  ["data storage", storageOf(runtime.storage.data)],
9691
9913
  ["engine storage", storageOf(runtime.storage.engine)],
9692
9914
  ["last reconcile", runtime.reconcile.lastTickError === null ? runtime.reconcile.lastTickAt ?? "never" : `${runtime.reconcile.lastTickAt ?? "never"}: ${runtime.reconcile.lastTickError}`],
9693
9915
  ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
9694
- ["log readers", String(runtime.ingest.readers)]
9916
+ ["log readers", String(runtime.ingest.readers)],
9917
+ ["oldest cursor lag", lagOf(runtime.ingest)],
9918
+ ...engine === "native" ? [] : sandboxRowsOf(runtime)
9695
9919
  ];
9696
9920
  const sessionsText = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
9697
9921
  const heldPoolsOf = ({ held = [] }) => held;
@@ -9725,7 +9949,7 @@ const nodeDetailsText = (node) => [
9725
9949
  ["data root", node.host?.dataRoot ?? "-"],
9726
9950
  ["capabilities", capabilitiesOf(node)],
9727
9951
  ["offers", node.offered.length === 0 ? "-" : node.offered.join(", ")],
9728
- ["split", splitSummaryOf(node.split)],
9952
+ ["split", splitSummaryOf(node.split, processorCountsOf(node))],
9729
9953
  ["registration", registrationOf(node)],
9730
9954
  ["connected", node.connectedAt ?? "never"],
9731
9955
  ["last seen", node.lastSeenAt ?? "never"],
@@ -9751,9 +9975,9 @@ const nodeDetailsText = (node) => [
9751
9975
  ])),
9752
9976
  "",
9753
9977
  ...node.runtime === null ? ["The node has reported no runtime yet."] : [
9754
- renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime)),
9978
+ renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime, node.engine)),
9755
9979
  "",
9756
- poolsText(node.runtime.pools)
9980
+ poolsText(node.runtime.pools, node.split)
9757
9981
  ]
9758
9982
  ].join("\n");
9759
9983
  const installCommandsOf = (origin) => [
@@ -9880,6 +10104,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
9880
10104
  return printed(run, await nodeDetailOf(run.api, reference), nodeDetailsText);
9881
10105
  }
9882
10106
  }),
10107
+ createNodeMetricsCommand(adaptersOf),
9883
10108
  createSplitCommand(adaptersOf),
9884
10109
  defineCommand({
9885
10110
  name: "create",
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "version": "1.2.0",
18
+ "version": "1.3.0",
19
19
  "dependencies": {
20
20
  "@clack/prompts": "1.8.1",
21
21
  "@modelcontextprotocol/server": "2.1.0",