@kici-dev/orchestrator 0.1.14 → 0.1.16
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/README.md +13 -1
- package/dist/__test-helpers__/mock-db.d.ts +2 -0
- package/dist/agent/dispatcher.d.ts +110 -6
- package/dist/agent/registry.d.ts +14 -0
- package/dist/app.d.ts +36 -0
- package/dist/approvals/apply-decision.d.ts +60 -0
- package/dist/approvals/approval-resolver.d.ts +66 -0
- package/dist/approvals/step-approval-bridge.d.ts +72 -0
- package/dist/approvals/team-membership-lookup.d.ts +13 -0
- package/dist/cache/agent-job-failed-error.d.ts +13 -0
- package/dist/cache/dispatch-cache-ref-tracker.d.ts +45 -0
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/user-cache.d.ts +116 -0
- package/dist/cancel/cancel-run.d.ts +56 -0
- package/dist/cli/api-client.d.ts +24 -0
- package/dist/cli/commands/environment.d.ts +1 -0
- package/dist/cli/commands/firecracker/index.d.ts +11 -0
- package/dist/cli/commands/firecracker/provision.d.ts +13 -0
- package/dist/cli/commands/firecracker/teardown.d.ts +3 -0
- package/dist/cli/commands/firecracker/verify.d.ts +3 -0
- package/dist/cli/commands/scaler.d.ts +18 -0
- package/dist/cli/kici-admin.d.ts +10 -1
- package/dist/cli/service/image-digests.d.ts +21 -0
- package/dist/cli.js +4148 -455
- package/dist/cluster/peer-client.d.ts +23 -1
- package/dist/cluster/peer-handler.d.ts +9 -1
- package/dist/cluster/peer-registry.d.ts +6 -0
- package/dist/config/schema.d.ts +4 -0
- package/dist/config.d.ts +13 -0
- package/dist/dashboard/handler.d.ts +92 -1
- package/dist/db/migrations/026_event_log_lockfile_corrupt.d.ts +11 -0
- package/dist/db/migrations/027_workflow_timeout.d.ts +20 -0
- package/dist/db/migrations/028_org_settings_user_cache.d.ts +4 -0
- package/dist/db/migrations/029_dispatch_queue_attempts.d.ts +16 -0
- package/dist/db/migrations/030_held_runs_env_set_null.d.ts +13 -0
- package/dist/db/migrations/031_dispatch_queue_ack_deadline.d.ts +19 -0
- package/dist/db/migrations/032_org_settings_dispatch_ack_timeout.d.ts +14 -0
- package/dist/db/migrations/033_org_settings_approval.d.ts +19 -0
- package/dist/db/migrations/034_held_runs_generalize.d.ts +24 -0
- package/dist/db/types.d.ts +89 -3
- package/dist/diagnostics/bundle-writer.d.ts +1 -17
- package/dist/diagnostics/checks/firecracker-network.d.ts +13 -0
- package/dist/diagnostics/checks/index.d.ts +2 -1
- package/dist/diagnostics/fleet-collector.d.ts +52 -0
- package/dist/diagnostics/fleet-constants.d.ts +8 -0
- package/dist/diagnostics/fleet-selection.d.ts +15 -0
- package/dist/diagnostics/fleet-topology.d.ts +47 -0
- package/dist/diagnostics/fleet-wiring.d.ts +60 -0
- package/dist/environments/environment-store.d.ts +14 -1
- package/dist/environments/held-runs.d.ts +69 -1
- package/dist/firecracker/host-network.d.ts +83 -0
- package/dist/firecracker/persist.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +380 -40
- package/dist/lockfile-cache.d.ts +1 -1
- package/dist/metrics/prometheus.d.ts +8 -0
- package/dist/orchestrator-core.d.ts +13 -2
- package/dist/pipeline/dispatch-matched-workflow.d.ts +9 -0
- package/dist/pipeline/inline-eval.d.ts +17 -2
- package/dist/pipeline/process-webhook.d.ts +19 -0
- package/dist/pipeline/processor.d.ts +28 -1
- package/dist/pipeline/test-pipeline.d.ts +10 -0
- package/dist/providers/github/lock-file.d.ts +1 -1
- package/dist/providers/internal/lock-file-fetcher.d.ts +3 -2
- package/dist/queue/job-queue.d.ts +53 -1
- package/dist/reporting/execution-tracker.d.ts +3 -1
- package/dist/routes/admin-environments.d.ts +1 -0
- package/dist/routes/fleet.d.ts +20 -0
- package/dist/scaler/bare-metal-backend.d.ts +1 -0
- package/dist/scaler/config.d.ts +4 -2
- package/dist/scaler/container-backend.d.ts +3 -2
- package/dist/scaler/disk-guard.d.ts +27 -0
- package/dist/scaler/firecracker-backend.d.ts +35 -0
- package/dist/scaler/manager.d.ts +8 -0
- package/dist/scaler/nftables.d.ts +29 -3
- package/dist/scaler/reap-orphans.d.ts +27 -0
- package/dist/scaler/types.d.ts +28 -1
- package/dist/server.js +6138 -1587
- package/dist/stale-detector/stale-run-detector.d.ts +18 -0
- package/dist/stale-detector/workflow-deadline-detector.d.ts +49 -0
- package/dist/standalone.js +18499 -14925
- package/dist/storage/filesystem.d.ts +14 -4
- package/dist/storage/s3.d.ts +19 -5
- package/dist/storage/types.d.ts +32 -5
- package/dist/worker/in-memory-job-queue.d.ts +40 -7
- package/dist/ws/agent-handler.d.ts +45 -0
- package/dist/ws/dashboard-env-handler.d.ts +36 -0
- package/dist/ws/fleet-agent-collector.d.ts +23 -0
- package/dist/ws/platform-client.d.ts +10 -1
- package/installer-image-digests.json +7 -0
- package/package.json +15 -11
- package/sbom.spdx.json +62 -57
package/dist/index.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { fileURLToPath as __cjs_fileURLToPath } from "node:url";
|
|
2
|
-
import { dirname as __cjs_dirname } from "node:path";
|
|
3
|
-
__cjs_dirname(__cjs_fileURLToPath(import.meta.url));
|
|
4
1
|
import "node:module";
|
|
5
|
-
import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
6
3
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
7
4
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
8
5
|
import { createLogger, createS3Client, sha256 } from "@kici-dev/shared";
|
|
9
6
|
import { promises } from "node:fs";
|
|
10
7
|
import { dirname, join } from "node:path";
|
|
11
|
-
import { createHmac, randomBytes } from "node:crypto";
|
|
8
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
12
9
|
import "node:fs/promises";
|
|
13
10
|
import { Kysely, PostgresDialect, sql } from "kysely";
|
|
14
11
|
import pg from "pg";
|
|
@@ -36,6 +33,8 @@ var S3CacheStorage = class {
|
|
|
36
33
|
client;
|
|
37
34
|
/** Separate client for pre-signed URL generation (uses externalEndpoint if configured). */
|
|
38
35
|
presignClient;
|
|
36
|
+
/** Separate client for the host CLI's pre-signed upload URL (uses uploadEndpoint if configured). */
|
|
37
|
+
uploadPresignClient;
|
|
39
38
|
bucket;
|
|
40
39
|
prefix;
|
|
41
40
|
ttlMs;
|
|
@@ -49,15 +48,21 @@ var S3CacheStorage = class {
|
|
|
49
48
|
endpoint: options.externalEndpoint
|
|
50
49
|
});
|
|
51
50
|
else this.presignClient = this.client;
|
|
51
|
+
if (options.uploadEndpoint) this.uploadPresignClient = createS3Client({
|
|
52
|
+
...options,
|
|
53
|
+
endpoint: options.uploadEndpoint
|
|
54
|
+
});
|
|
55
|
+
else this.uploadPresignClient = this.client;
|
|
52
56
|
}
|
|
53
57
|
/** Build the full S3 object key from a cache key. */
|
|
54
58
|
objectKey(key) {
|
|
55
59
|
return `${this.prefix}${key}`;
|
|
56
60
|
}
|
|
57
|
-
/** Check if metadata indicates an expired item. */
|
|
58
|
-
isExpired(meta) {
|
|
61
|
+
/** Check if metadata indicates an expired item, honoring a per-op TTL override. */
|
|
62
|
+
isExpired(meta, ttlMsOverride) {
|
|
63
|
+
const ttl = ttlMsOverride ?? this.ttlMs;
|
|
59
64
|
const lastAccessed = new Date(meta.lastAccessedAt).getTime();
|
|
60
|
-
return Date.now() - lastAccessed >
|
|
65
|
+
return Date.now() - lastAccessed > ttl;
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Read metadata from an S3 object's custom headers.
|
|
@@ -121,10 +126,10 @@ var S3CacheStorage = class {
|
|
|
121
126
|
}
|
|
122
127
|
}).done();
|
|
123
128
|
}
|
|
124
|
-
async get(key) {
|
|
129
|
+
async get(key, ttlMsOverride) {
|
|
125
130
|
const meta = await this.readMeta(key);
|
|
126
131
|
if (!meta) return null;
|
|
127
|
-
if (this.isExpired(meta)) {
|
|
132
|
+
if (this.isExpired(meta, ttlMsOverride)) {
|
|
128
133
|
await this.deleteObject(key);
|
|
129
134
|
return null;
|
|
130
135
|
}
|
|
@@ -146,10 +151,10 @@ var S3CacheStorage = class {
|
|
|
146
151
|
} catch {}
|
|
147
152
|
return data;
|
|
148
153
|
}
|
|
149
|
-
async has(key) {
|
|
154
|
+
async has(key, ttlMsOverride) {
|
|
150
155
|
const meta = await this.readMeta(key);
|
|
151
156
|
if (!meta) return false;
|
|
152
|
-
if (this.isExpired(meta)) {
|
|
157
|
+
if (this.isExpired(meta, ttlMsOverride)) {
|
|
153
158
|
await this.deleteObject(key);
|
|
154
159
|
return false;
|
|
155
160
|
}
|
|
@@ -166,10 +171,10 @@ var S3CacheStorage = class {
|
|
|
166
171
|
meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
167
172
|
await this.updateMeta(key, meta);
|
|
168
173
|
}
|
|
169
|
-
async getUrl(key) {
|
|
174
|
+
async getUrl(key, ttlMsOverride) {
|
|
170
175
|
const meta = await this.readMeta(key);
|
|
171
176
|
if (!meta) return null;
|
|
172
|
-
if (this.isExpired(meta)) {
|
|
177
|
+
if (this.isExpired(meta, ttlMsOverride)) {
|
|
173
178
|
await this.deleteObject(key);
|
|
174
179
|
return null;
|
|
175
180
|
}
|
|
@@ -187,7 +192,7 @@ var S3CacheStorage = class {
|
|
|
187
192
|
}
|
|
188
193
|
async getInternalUploadUrl(key) {
|
|
189
194
|
const objectKey = this.objectKey(key);
|
|
190
|
-
return getSignedUrl(this.
|
|
195
|
+
return getSignedUrl(this.uploadPresignClient, new PutObjectCommand({
|
|
191
196
|
Bucket: this.bucket,
|
|
192
197
|
Key: objectKey
|
|
193
198
|
}), { expiresIn: UPLOAD_URL_EXPIRY_SECONDS });
|
|
@@ -206,6 +211,46 @@ var S3CacheStorage = class {
|
|
|
206
211
|
}
|
|
207
212
|
}));
|
|
208
213
|
}
|
|
214
|
+
async list(subPrefix) {
|
|
215
|
+
const fullPrefix = this.objectKey(subPrefix);
|
|
216
|
+
const items = [];
|
|
217
|
+
let token;
|
|
218
|
+
do {
|
|
219
|
+
const resp = await this.client.send(new ListObjectsV2Command({
|
|
220
|
+
Bucket: this.bucket,
|
|
221
|
+
Prefix: fullPrefix,
|
|
222
|
+
ContinuationToken: token
|
|
223
|
+
}));
|
|
224
|
+
for (const obj of resp.Contents ?? []) {
|
|
225
|
+
if (!obj.Key) continue;
|
|
226
|
+
items.push({
|
|
227
|
+
key: obj.Key.slice(this.prefix.length),
|
|
228
|
+
created: obj.LastModified ? obj.LastModified.getTime() : 0
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
token = resp.IsTruncated ? resp.NextContinuationToken : void 0;
|
|
232
|
+
} while (token);
|
|
233
|
+
items.sort((a, b) => b.created - a.created);
|
|
234
|
+
return items.map((i) => i.key);
|
|
235
|
+
}
|
|
236
|
+
async getMetadata(key) {
|
|
237
|
+
return this.readMeta(key);
|
|
238
|
+
}
|
|
239
|
+
async copy(srcKey, destKey) {
|
|
240
|
+
const srcObj = this.objectKey(srcKey);
|
|
241
|
+
const destObj = this.objectKey(destKey);
|
|
242
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
243
|
+
await this.client.send(new CopyObjectCommand({
|
|
244
|
+
Bucket: this.bucket,
|
|
245
|
+
Key: destObj,
|
|
246
|
+
CopySource: `${this.bucket}/${srcObj}`,
|
|
247
|
+
MetadataDirective: "REPLACE",
|
|
248
|
+
Metadata: {
|
|
249
|
+
"created-at": now,
|
|
250
|
+
"last-accessed-at": now
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
209
254
|
/** Delete an S3 object. Idempotent (doesn't error if missing). */
|
|
210
255
|
async deleteObject(key) {
|
|
211
256
|
try {
|
|
@@ -298,9 +343,10 @@ var FilesystemCacheStorage = class {
|
|
|
298
343
|
metaPath(key) {
|
|
299
344
|
return `${this.resolvePath(key)}.meta.json`;
|
|
300
345
|
}
|
|
301
|
-
isExpired(meta) {
|
|
346
|
+
isExpired(meta, ttlMsOverride) {
|
|
347
|
+
const ttl = ttlMsOverride ?? this.ttlMs;
|
|
302
348
|
const lastAccessed = new Date(meta.lastAccessedAt).getTime();
|
|
303
|
-
return Date.now() - lastAccessed >
|
|
349
|
+
return Date.now() - lastAccessed > ttl;
|
|
304
350
|
}
|
|
305
351
|
async readMeta(key) {
|
|
306
352
|
try {
|
|
@@ -328,10 +374,10 @@ var FilesystemCacheStorage = class {
|
|
|
328
374
|
lastAccessedAt: now
|
|
329
375
|
});
|
|
330
376
|
}
|
|
331
|
-
async get(key) {
|
|
377
|
+
async get(key, ttlMsOverride) {
|
|
332
378
|
const meta = await this.readMeta(key);
|
|
333
379
|
if (!meta) return null;
|
|
334
|
-
if (this.isExpired(meta)) {
|
|
380
|
+
if (this.isExpired(meta, ttlMsOverride)) {
|
|
335
381
|
await this.deleteFiles(key);
|
|
336
382
|
return null;
|
|
337
383
|
}
|
|
@@ -348,10 +394,10 @@ var FilesystemCacheStorage = class {
|
|
|
348
394
|
} catch {}
|
|
349
395
|
return data;
|
|
350
396
|
}
|
|
351
|
-
async has(key) {
|
|
397
|
+
async has(key, ttlMsOverride) {
|
|
352
398
|
const meta = await this.readMeta(key);
|
|
353
399
|
if (!meta) return false;
|
|
354
|
-
if (this.isExpired(meta)) {
|
|
400
|
+
if (this.isExpired(meta, ttlMsOverride)) {
|
|
355
401
|
await this.deleteFiles(key);
|
|
356
402
|
return false;
|
|
357
403
|
}
|
|
@@ -368,10 +414,10 @@ var FilesystemCacheStorage = class {
|
|
|
368
414
|
meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
369
415
|
await this.writeMeta(key, meta);
|
|
370
416
|
}
|
|
371
|
-
async getUrl(key) {
|
|
417
|
+
async getUrl(key, ttlMsOverride) {
|
|
372
418
|
const meta = await this.readMeta(key);
|
|
373
419
|
if (!meta) return null;
|
|
374
|
-
if (this.isExpired(meta)) {
|
|
420
|
+
if (this.isExpired(meta, ttlMsOverride)) {
|
|
375
421
|
await this.deleteFiles(key);
|
|
376
422
|
return null;
|
|
377
423
|
}
|
|
@@ -390,6 +436,57 @@ var FilesystemCacheStorage = class {
|
|
|
390
436
|
lastAccessedAt: now
|
|
391
437
|
});
|
|
392
438
|
}
|
|
439
|
+
async list(subPrefix) {
|
|
440
|
+
const root = this.resolvePath(subPrefix.replace(/\/$/, ""));
|
|
441
|
+
let entries;
|
|
442
|
+
try {
|
|
443
|
+
entries = await this.walkDataFiles(root, subPrefix.replace(/\/$/, ""));
|
|
444
|
+
} catch (err) {
|
|
445
|
+
if (isNotFoundFsError(err)) return [];
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
449
|
+
return entries.map((e) => e.key);
|
|
450
|
+
}
|
|
451
|
+
async getMetadata(key) {
|
|
452
|
+
return this.readMeta(key);
|
|
453
|
+
}
|
|
454
|
+
async copy(srcKey, destKey) {
|
|
455
|
+
const srcPath = this.resolvePath(srcKey);
|
|
456
|
+
const destPath = this.resolvePath(destKey);
|
|
457
|
+
await promises.mkdir(dirname(destPath), { recursive: true });
|
|
458
|
+
await promises.copyFile(srcPath, destPath);
|
|
459
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
460
|
+
await this.writeMeta(destKey, {
|
|
461
|
+
createdAt: now,
|
|
462
|
+
lastAccessedAt: now
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Recursively collect data files under `dir`, returning each as a cache key
|
|
467
|
+
* relative to `basePath` paired with its mtime. Metadata sidecars
|
|
468
|
+
* (`*.meta.json`) and the atomic-write temp files (`*.tmp-*`) are skipped so
|
|
469
|
+
* `list()` only surfaces real cache objects.
|
|
470
|
+
*/
|
|
471
|
+
async walkDataFiles(dir, keyPrefix) {
|
|
472
|
+
const out = [];
|
|
473
|
+
const dirents = await promises.readdir(dir, { withFileTypes: true });
|
|
474
|
+
for (const dirent of dirents) {
|
|
475
|
+
const childPath = join(dir, dirent.name);
|
|
476
|
+
const childKey = keyPrefix ? `${keyPrefix}/${dirent.name}` : dirent.name;
|
|
477
|
+
if (dirent.isDirectory()) {
|
|
478
|
+
out.push(...await this.walkDataFiles(childPath, childKey));
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (dirent.name.endsWith(".meta.json") || /\.tmp-[0-9a-f]+$/.test(dirent.name)) continue;
|
|
482
|
+
const stat = await promises.stat(childPath);
|
|
483
|
+
out.push({
|
|
484
|
+
key: childKey,
|
|
485
|
+
mtime: stat.mtimeMs
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
393
490
|
signedUrl(method, key) {
|
|
394
491
|
const { token } = signToken(this.signingSecret, method, key);
|
|
395
492
|
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
@@ -432,6 +529,7 @@ function createCacheStorage(config) {
|
|
|
432
529
|
region: config.region,
|
|
433
530
|
endpoint: config.endpoint,
|
|
434
531
|
externalEndpoint: config.externalEndpoint,
|
|
532
|
+
uploadEndpoint: config.uploadEndpoint,
|
|
435
533
|
forcePathStyle: config.forcePathStyle
|
|
436
534
|
});
|
|
437
535
|
if (config.type === "filesystem") return new FilesystemCacheStorage({
|
|
@@ -460,7 +558,7 @@ function createCacheStorage(config) {
|
|
|
460
558
|
* downloads the source tarball, extracts it, and imports the workflow entry
|
|
461
559
|
* via the shared oxc-transform ESM loader hook.
|
|
462
560
|
*/
|
|
463
|
-
const logger$
|
|
561
|
+
const logger$2 = createLogger({ prefix: "source-cache" });
|
|
464
562
|
/** Cache key format: source/{contentHash}.tar.gz */
|
|
465
563
|
function sourceKey(contentHash) {
|
|
466
564
|
return `source/${contentHash}.tar.gz`;
|
|
@@ -473,7 +571,7 @@ var SourceCache = class {
|
|
|
473
571
|
async has(contentHash) {
|
|
474
572
|
const key = sourceKey(contentHash);
|
|
475
573
|
const exists = await this.storage.has(key);
|
|
476
|
-
logger$
|
|
574
|
+
logger$2.debug(`has(${contentHash}): ${exists}`);
|
|
477
575
|
return exists;
|
|
478
576
|
}
|
|
479
577
|
async get(contentHash) {
|
|
@@ -481,8 +579,8 @@ var SourceCache = class {
|
|
|
481
579
|
const data = await this.storage.get(key);
|
|
482
580
|
if (data) {
|
|
483
581
|
await this.storage.touch(key);
|
|
484
|
-
logger$
|
|
485
|
-
} else logger$
|
|
582
|
+
logger$2.debug(`get(${contentHash}): hit (${data.length} bytes)`);
|
|
583
|
+
} else logger$2.debug(`get(${contentHash}): miss`);
|
|
486
584
|
return data;
|
|
487
585
|
}
|
|
488
586
|
async getUrl(contentHash) {
|
|
@@ -490,8 +588,8 @@ var SourceCache = class {
|
|
|
490
588
|
const url = await this.storage.getUrl(key);
|
|
491
589
|
if (url) {
|
|
492
590
|
await this.storage.touch(key);
|
|
493
|
-
logger$
|
|
494
|
-
} else logger$
|
|
591
|
+
logger$2.debug(`getUrl(${contentHash}): hit`);
|
|
592
|
+
} else logger$2.debug(`getUrl(${contentHash}): miss`);
|
|
495
593
|
return url;
|
|
496
594
|
}
|
|
497
595
|
async getUploadUrl(contentHash) {
|
|
@@ -502,12 +600,12 @@ var SourceCache = class {
|
|
|
502
600
|
const key = sourceKey(contentHash);
|
|
503
601
|
await this.storage.put(key, tarball);
|
|
504
602
|
const size = typeof tarball === "string" ? Buffer.byteLength(tarball) : tarball.length;
|
|
505
|
-
logger$
|
|
603
|
+
logger$2.info(`store(${contentHash}): stored (${size} bytes)`);
|
|
506
604
|
}
|
|
507
605
|
async remove(contentHash) {
|
|
508
606
|
const key = sourceKey(contentHash);
|
|
509
607
|
const removed = await this.storage.delete(key);
|
|
510
|
-
logger$
|
|
608
|
+
logger$2.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`);
|
|
511
609
|
return removed;
|
|
512
610
|
}
|
|
513
611
|
};
|
|
@@ -522,7 +620,7 @@ var SourceCache = class {
|
|
|
522
620
|
*
|
|
523
621
|
* Cache key format: deps/{platform}-{arch}/{lockfileHash}.tar.gz
|
|
524
622
|
*/
|
|
525
|
-
const logger = createLogger({ prefix: "dep-cache" });
|
|
623
|
+
const logger$1 = createLogger({ prefix: "dep-cache" });
|
|
526
624
|
/** Default max tarball size: 500MB */
|
|
527
625
|
const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
|
|
528
626
|
/** Build cache key for dependency tarball: deps/{platform}-{arch}/{lockfileHash}.tar.gz */
|
|
@@ -540,7 +638,7 @@ var DepCache = class {
|
|
|
540
638
|
async has(lockfileHash, platform, arch) {
|
|
541
639
|
const key = depKey(lockfileHash, platform, arch);
|
|
542
640
|
const exists = await this.storage.has(key);
|
|
543
|
-
logger.debug(`has(${lockfileHash}): ${exists}`, {
|
|
641
|
+
logger$1.debug(`has(${lockfileHash}): ${exists}`, {
|
|
544
642
|
platform,
|
|
545
643
|
arch
|
|
546
644
|
});
|
|
@@ -555,11 +653,11 @@ var DepCache = class {
|
|
|
555
653
|
const url = await this.storage.getUrl(key);
|
|
556
654
|
if (url) {
|
|
557
655
|
await this.storage.touch(key);
|
|
558
|
-
logger.debug(`getUrl(${lockfileHash}): hit`, {
|
|
656
|
+
logger$1.debug(`getUrl(${lockfileHash}): hit`, {
|
|
559
657
|
platform,
|
|
560
658
|
arch
|
|
561
659
|
});
|
|
562
|
-
} else logger.debug(`getUrl(${lockfileHash}): miss`, {
|
|
660
|
+
} else logger$1.debug(`getUrl(${lockfileHash}): miss`, {
|
|
563
661
|
platform,
|
|
564
662
|
arch
|
|
565
663
|
});
|
|
@@ -574,7 +672,7 @@ var DepCache = class {
|
|
|
574
672
|
const key = depKey(lockfileHash, platform, arch);
|
|
575
673
|
const url = await this.storage.getUrl(key);
|
|
576
674
|
if (!url) {
|
|
577
|
-
logger.debug(`getUrlAndHash(${lockfileHash}): miss`, {
|
|
675
|
+
logger$1.debug(`getUrlAndHash(${lockfileHash}): miss`, {
|
|
578
676
|
platform,
|
|
579
677
|
arch
|
|
580
678
|
});
|
|
@@ -583,7 +681,7 @@ var DepCache = class {
|
|
|
583
681
|
await this.storage.touch(key);
|
|
584
682
|
const hashKey = `deps/${platform}-${arch}/${lockfileHash}.hash`;
|
|
585
683
|
const hash = (await this.storage.get(hashKey))?.toString("utf-8") || void 0;
|
|
586
|
-
logger.debug(`getUrlAndHash(${lockfileHash}): hit`, {
|
|
684
|
+
logger$1.debug(`getUrlAndHash(${lockfileHash}): hit`, {
|
|
587
685
|
platform,
|
|
588
686
|
arch,
|
|
589
687
|
hasHash: !!hash
|
|
@@ -608,7 +706,7 @@ var DepCache = class {
|
|
|
608
706
|
if (tarballData.length > this.maxTarballBytes) throw new Error(`Dep tarball exceeds max size: ${tarballData.length} bytes > ${this.maxTarballBytes} bytes limit`);
|
|
609
707
|
const key = depKey(lockfileHash, platform, arch);
|
|
610
708
|
await this.storage.put(key, tarballData);
|
|
611
|
-
logger.info(`store: ${tarballData.length} bytes`, {
|
|
709
|
+
logger$1.info(`store: ${tarballData.length} bytes`, {
|
|
612
710
|
lockfileHash,
|
|
613
711
|
platform,
|
|
614
712
|
arch
|
|
@@ -625,7 +723,7 @@ var DepCache = class {
|
|
|
625
723
|
async remove(lockfileHash, platform, arch) {
|
|
626
724
|
const key = depKey(lockfileHash, platform, arch);
|
|
627
725
|
const removed = await this.storage.delete(key);
|
|
628
|
-
logger.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
|
|
726
|
+
logger$1.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
|
|
629
727
|
platform,
|
|
630
728
|
arch
|
|
631
729
|
});
|
|
@@ -633,6 +731,248 @@ var DepCache = class {
|
|
|
633
731
|
}
|
|
634
732
|
};
|
|
635
733
|
//#endregion
|
|
734
|
+
//#region src/cache/user-cache.ts
|
|
735
|
+
/**
|
|
736
|
+
* User-facing cache layer wrapping CacheStorage.
|
|
737
|
+
*
|
|
738
|
+
* Namespacing: `cache/<orgId>/<repoId>/<refScope>/<key>.tar.gz`, where
|
|
739
|
+
* `refScope` is `shared` for trusted refs (org-shared, default-branch cache)
|
|
740
|
+
* or `iso/<runId>` for untrusted refs (per-run isolated scope). Restores from
|
|
741
|
+
* an untrusted ref read the shared scope as a fallback but writes can NEVER
|
|
742
|
+
* land in the shared scope — the GitHub Actions cache-isolation model. Keyed
|
|
743
|
+
* strictly per org so no tenant can read another tenant's cache.
|
|
744
|
+
*
|
|
745
|
+
* Saves are immutable (first save under an exact key wins) and atomic
|
|
746
|
+
* (upload to a `.tmp-<uuid>` key, then server-side copy to the final key and
|
|
747
|
+
* delete the temp), so a crashed save never leaves a corrupt final entry.
|
|
748
|
+
*
|
|
749
|
+
* Eviction: per-org byte quota plus the TTL the backing CacheStorage already
|
|
750
|
+
* enforces lazily on access. On a save that pushes the org over quota,
|
|
751
|
+
* least-recently-used entries (oldest `lastAccessedAt`, read from the storage
|
|
752
|
+
* backend's metadata) are evicted until under quota; each eviction is logged.
|
|
753
|
+
* The companion `.hash` / `.size` sidecar objects carry the integrity
|
|
754
|
+
* hash and size accounting outside the tarball's own (presigned, metadata-less)
|
|
755
|
+
* upload.
|
|
756
|
+
*/
|
|
757
|
+
const logger = createLogger({ prefix: "user-cache" });
|
|
758
|
+
/**
|
|
759
|
+
* Cluster-wide default quota: 5 GiB. Serves as the fallback when an org has no
|
|
760
|
+
* per-org override in `org_settings.user_cache_quota_bytes`. The cluster-wide
|
|
761
|
+
* value is itself operator-configurable via KICI_USER_CACHE_QUOTA_BYTES.
|
|
762
|
+
*/
|
|
763
|
+
const DEFAULT_USER_CACHE_QUOTA_BYTES = 5 * 1024 * 1024 * 1024;
|
|
764
|
+
/**
|
|
765
|
+
* Cluster-wide default entry TTL: 7 days. Fallback when an org has no per-org
|
|
766
|
+
* override in `org_settings.user_cache_ttl_ms`. The cluster-wide value is
|
|
767
|
+
* operator-configurable via KICI_USER_CACHE_TTL_MS.
|
|
768
|
+
*/
|
|
769
|
+
const DEFAULT_USER_CACHE_TTL_MS = 10080 * 60 * 1e3;
|
|
770
|
+
/** Tarball suffix for committed cache entries. */
|
|
771
|
+
const TAR_SUFFIX = ".tar.gz";
|
|
772
|
+
var UserCache = class {
|
|
773
|
+
storage;
|
|
774
|
+
/** Cluster-wide default quota (the `KICI_USER_CACHE_QUOTA_BYTES` value). */
|
|
775
|
+
defaultQuotaBytes;
|
|
776
|
+
/** Cluster-wide default TTL (the `KICI_USER_CACHE_TTL_MS` value). */
|
|
777
|
+
defaultTtlMs;
|
|
778
|
+
/** Optional per-org override reader; absent = always use the cluster defaults. */
|
|
779
|
+
orgLimitsReader;
|
|
780
|
+
constructor(opts) {
|
|
781
|
+
this.storage = opts.storage;
|
|
782
|
+
this.defaultQuotaBytes = opts.quotaBytes ?? 5368709120;
|
|
783
|
+
this.defaultTtlMs = opts.ttlMs ?? 6048e5;
|
|
784
|
+
this.orgLimitsReader = opts.orgLimitsReader;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Resolve the effective quota + TTL for an org: the per-org override from
|
|
788
|
+
* `org_settings` when present, otherwise the cluster-wide default. A reader
|
|
789
|
+
* failure falls back to the defaults (logged) — the cache must never fail a
|
|
790
|
+
* restore/save because the settings lookup hiccupped.
|
|
791
|
+
*/
|
|
792
|
+
async resolveLimits(org) {
|
|
793
|
+
if (!this.orgLimitsReader) return {
|
|
794
|
+
quotaBytes: this.defaultQuotaBytes,
|
|
795
|
+
ttlMs: this.defaultTtlMs
|
|
796
|
+
};
|
|
797
|
+
let limits = {};
|
|
798
|
+
try {
|
|
799
|
+
limits = await this.orgLimitsReader(org);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
logger.warn("user-cache org-limits lookup failed — using cluster defaults", {
|
|
802
|
+
org,
|
|
803
|
+
error: err instanceof Error ? err.message : String(err)
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
quotaBytes: limits.quotaBytes ?? this.defaultQuotaBytes,
|
|
808
|
+
ttlMs: limits.ttlMs ?? this.defaultTtlMs
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Sanitize a path segment so a key can never escape its org/repo/scope
|
|
813
|
+
* namespace. Beyond stripping disallowed characters, a segment consisting
|
|
814
|
+
* only of dots (`.`, `..`, …) is replaced wholesale: such a segment is a
|
|
815
|
+
* dot-segment that HTTP/S3 path canonicalization collapses (`a/./b` → `a/b`,
|
|
816
|
+
* `a/../b` → `b`), which both corrupts the namespace and breaks the SigV4
|
|
817
|
+
* signature on a pre-signed PUT/GET. Repo identifiers like `.` (the internal
|
|
818
|
+
* provider's repo id) hit exactly this case, so the all-dots guard keeps the
|
|
819
|
+
* object key canonical and the namespace boundary intact.
|
|
820
|
+
*/
|
|
821
|
+
seg(s) {
|
|
822
|
+
const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
823
|
+
return /^\.+$/.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
824
|
+
}
|
|
825
|
+
/** Org-level prefix: the per-tenant isolation boundary and quota scope. */
|
|
826
|
+
orgPrefix(ref) {
|
|
827
|
+
return `cache/${this.seg(ref.org)}/`;
|
|
828
|
+
}
|
|
829
|
+
/** Org + repo prefix shared by every scope of a repo. */
|
|
830
|
+
repoPrefix(ref) {
|
|
831
|
+
return `${this.orgPrefix(ref)}${this.seg(ref.repo)}`;
|
|
832
|
+
}
|
|
833
|
+
/** Namespace prefix for the WRITE scope of a ref (shared OR per-run isolated). */
|
|
834
|
+
writePrefix(ref) {
|
|
835
|
+
const base = this.repoPrefix(ref);
|
|
836
|
+
if (ref.scope === "isolated") {
|
|
837
|
+
if (!ref.runId) throw new Error("isolated cache scope requires a runId");
|
|
838
|
+
return `${base}/iso/${this.seg(ref.runId)}/`;
|
|
839
|
+
}
|
|
840
|
+
return `${base}/shared/`;
|
|
841
|
+
}
|
|
842
|
+
/** Namespace prefixes the ref may READ, in priority order. Isolated reads its own run scope, then shared. */
|
|
843
|
+
readPrefixes(ref) {
|
|
844
|
+
const base = this.repoPrefix(ref);
|
|
845
|
+
if (ref.scope === "isolated") {
|
|
846
|
+
if (!ref.runId) throw new Error("isolated cache scope requires a runId");
|
|
847
|
+
return [`${base}/iso/${this.seg(ref.runId)}/`, `${base}/shared/`];
|
|
848
|
+
}
|
|
849
|
+
return [`${base}/shared/`];
|
|
850
|
+
}
|
|
851
|
+
finalKey(prefix, key) {
|
|
852
|
+
return `${prefix}${this.seg(key)}${TAR_SUFFIX}`;
|
|
853
|
+
}
|
|
854
|
+
/** Restore: try the exact key across read prefixes, then restoreKeys prefix scan (newest wins). */
|
|
855
|
+
async restore(ref) {
|
|
856
|
+
const { ttlMs } = await this.resolveLimits(ref.org);
|
|
857
|
+
const prefixes = this.readPrefixes(ref);
|
|
858
|
+
const exact = await this.restoreExact(ref, prefixes, ttlMs);
|
|
859
|
+
if (exact) return exact;
|
|
860
|
+
return await this.restoreByPrefix(ref, prefixes, ttlMs) ?? { hit: false };
|
|
861
|
+
}
|
|
862
|
+
/** Try the exact key in read-prefix priority order. */
|
|
863
|
+
async restoreExact(ref, prefixes, ttlMs) {
|
|
864
|
+
for (const prefix of prefixes) {
|
|
865
|
+
const key = this.finalKey(prefix, ref.key);
|
|
866
|
+
const url = await this.storage.getUrl(key, ttlMs);
|
|
867
|
+
if (url) {
|
|
868
|
+
await this.storage.touch(key);
|
|
869
|
+
return {
|
|
870
|
+
hit: true,
|
|
871
|
+
matchedKey: ref.key,
|
|
872
|
+
downloadUrl: url,
|
|
873
|
+
tarHash: await this.readHash(key)
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
/** restoreKeys prefix fallback (ordered); within a prefix, list() returns newest-first. */
|
|
880
|
+
async restoreByPrefix(ref, prefixes, ttlMs) {
|
|
881
|
+
for (const rk of ref.restoreKeys ?? []) for (const prefix of prefixes) {
|
|
882
|
+
const matches = (await this.storage.list(`${prefix}${this.seg(rk)}`)).filter((k) => k.endsWith(TAR_SUFFIX));
|
|
883
|
+
if (matches.length === 0) continue;
|
|
884
|
+
const winner = matches[0];
|
|
885
|
+
const url = await this.storage.getUrl(winner, ttlMs);
|
|
886
|
+
if (!url) continue;
|
|
887
|
+
await this.storage.touch(winner);
|
|
888
|
+
return {
|
|
889
|
+
hit: true,
|
|
890
|
+
matchedKey: winner.slice(prefix.length, -7),
|
|
891
|
+
downloadUrl: url,
|
|
892
|
+
tarHash: await this.readHash(winner)
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
/** Begin a save: presigned PUT to a temp key, or skip=true when the immutable key exists. */
|
|
898
|
+
async beginSave(ref) {
|
|
899
|
+
const prefix = this.writePrefix(ref);
|
|
900
|
+
const final = this.finalKey(prefix, ref.key);
|
|
901
|
+
if (await this.storage.has(final)) {
|
|
902
|
+
logger.info("user-cache save skipped (immutable key exists)", { key: ref.key });
|
|
903
|
+
return { skip: true };
|
|
904
|
+
}
|
|
905
|
+
const tempKey = `${prefix}.tmp-${randomUUID()}${TAR_SUFFIX}`;
|
|
906
|
+
return {
|
|
907
|
+
skip: false,
|
|
908
|
+
uploadUrl: await this.storage.getUploadUrl(tempKey),
|
|
909
|
+
tempKey
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/** Commit a save: copy temp -> final, init metadata, store companion hash/size, delete temp, enforce quota. */
|
|
913
|
+
async commitSave(ref) {
|
|
914
|
+
const prefix = this.writePrefix(ref);
|
|
915
|
+
const final = this.finalKey(prefix, ref.key);
|
|
916
|
+
if (await this.storage.has(final)) return;
|
|
917
|
+
if (ref.tempKey) {
|
|
918
|
+
await this.storage.copy(ref.tempKey, final);
|
|
919
|
+
await this.storage.delete(ref.tempKey);
|
|
920
|
+
}
|
|
921
|
+
await this.storage.initMeta(final);
|
|
922
|
+
await this.storage.put(`${final}.hash`, ref.tarHash);
|
|
923
|
+
await this.storage.put(`${final}.size`, String(ref.sizeBytes));
|
|
924
|
+
logger.info("user-cache entry committed", {
|
|
925
|
+
key: ref.key,
|
|
926
|
+
sizeBytes: ref.sizeBytes
|
|
927
|
+
});
|
|
928
|
+
await this.enforceQuota(ref);
|
|
929
|
+
}
|
|
930
|
+
async readHash(key) {
|
|
931
|
+
return (await this.storage.get(`${key}.hash`))?.toString("utf-8") || void 0;
|
|
932
|
+
}
|
|
933
|
+
/** Evict least-recently-used entries for the org until total tarball size <= the per-org quota. */
|
|
934
|
+
async enforceQuota(ref) {
|
|
935
|
+
const { quotaBytes } = await this.resolveLimits(ref.org);
|
|
936
|
+
const orgPrefix = this.orgPrefix(ref);
|
|
937
|
+
const keys = (await this.storage.list(orgPrefix)).filter((k) => k.endsWith(TAR_SUFFIX));
|
|
938
|
+
const sized = [];
|
|
939
|
+
let total = 0;
|
|
940
|
+
for (const k of keys) {
|
|
941
|
+
const sizeData = await this.storage.get(`${k}.size`);
|
|
942
|
+
const size = sizeData ? Number(sizeData.toString("utf-8")) : 0;
|
|
943
|
+
total += size;
|
|
944
|
+
sized.push({
|
|
945
|
+
key: k,
|
|
946
|
+
size
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (total <= quotaBytes) return;
|
|
950
|
+
const candidates = [];
|
|
951
|
+
for (const e of sized) {
|
|
952
|
+
const meta = await this.storage.getMetadata(e.key);
|
|
953
|
+
const lastAccessed = meta ? new Date(meta.lastAccessedAt).getTime() : 0;
|
|
954
|
+
candidates.push({
|
|
955
|
+
key: e.key,
|
|
956
|
+
size: e.size,
|
|
957
|
+
lastAccessed
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
candidates.sort((a, b) => a.lastAccessed - b.lastAccessed);
|
|
961
|
+
for (const { key, size } of candidates) {
|
|
962
|
+
if (total <= quotaBytes) break;
|
|
963
|
+
await this.storage.delete(key);
|
|
964
|
+
await this.storage.delete(`${key}.hash`);
|
|
965
|
+
await this.storage.delete(`${key}.size`);
|
|
966
|
+
total -= size;
|
|
967
|
+
logger.info("user-cache eviction (org over quota)", {
|
|
968
|
+
org: ref.org,
|
|
969
|
+
key,
|
|
970
|
+
freedBytes: size
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
//#endregion
|
|
636
976
|
//#region src/cluster/peer-credentials.ts
|
|
637
977
|
/**
|
|
638
978
|
* Peer credential management for cluster authentication.
|
|
@@ -767,6 +1107,6 @@ async function createPeerCredentialStoreFromUrl(databaseUrl, opts) {
|
|
|
767
1107
|
};
|
|
768
1108
|
}
|
|
769
1109
|
//#endregion
|
|
770
|
-
export { DepCache, PeerCredentialStore, S3CacheStorage, SourceCache, createCacheStorage, createPeerCredentialStoreFromUrl };
|
|
1110
|
+
export { DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, PeerCredentialStore, S3CacheStorage, SourceCache, UserCache, createCacheStorage, createPeerCredentialStoreFromUrl };
|
|
771
1111
|
|
|
772
1112
|
//# sourceMappingURL=index.js.map
|
package/dist/lockfile-cache.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Replaces the old GitHub-specific LockFileCache in github/lockfile.ts.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
10
|
+
import { type LockFileFetcher, type LockFile } from '@kici-dev/engine';
|
|
11
11
|
export declare class LockFileCache {
|
|
12
12
|
private readonly cache;
|
|
13
13
|
private hits;
|
|
@@ -64,6 +64,14 @@ export declare const triggerMatchDurationSeconds: import("@opentelemetry/api").H
|
|
|
64
64
|
* Total deduplication cache hits (duplicate webhooks rejected).
|
|
65
65
|
*/
|
|
66
66
|
export declare const dedupHitsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
67
|
+
/**
|
|
68
|
+
* pg connection errors absorbed by the pool error handlers instead of
|
|
69
|
+
* crashing the process (e.g. a database failover terminating idle pooled
|
|
70
|
+
* backends).
|
|
71
|
+
* Labels:
|
|
72
|
+
* - source: idle-pool | client
|
|
73
|
+
*/
|
|
74
|
+
export declare const pgPoolClientErrorsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
67
75
|
/**
|
|
68
76
|
* Total source cache hits (`.kici/` source tarball found for content hash).
|
|
69
77
|
*/
|
|
@@ -22,11 +22,11 @@ import { DedupCache } from './webhook/dedup.js';
|
|
|
22
22
|
import { ObserverRegistry } from './ws/observer-registry.js';
|
|
23
23
|
import { AgentMetricsAggregator } from './metrics/agent-metrics-aggregator.js';
|
|
24
24
|
import { SourceLocationStore } from './app.js';
|
|
25
|
-
import { type PeerHeartbeat } from '@kici-dev/engine';
|
|
25
|
+
import { type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
|
|
26
26
|
import { ScalerManager } from './scaler/index.js';
|
|
27
27
|
import type { ScalerConfig } from './scaler/index.js';
|
|
28
28
|
import type { CacheStorage } from './storage/index.js';
|
|
29
|
-
import { SourceCache, BuildCoordinator, DepCache, PendingBuildTracker, PendingInitTracker, PendingDynamicTracker } from './cache/index.js';
|
|
29
|
+
import { SourceCache, BuildCoordinator, DepCache, UserCache, DispatchCacheRefTracker, PendingBuildTracker, PendingInitTracker, PendingDynamicTracker } from './cache/index.js';
|
|
30
30
|
import { CheckRunReporter } from './reporting/check-run-reporter.js';
|
|
31
31
|
import { StepLogBuffer } from './reporting/step-log-buffer.js';
|
|
32
32
|
import { type LogStorage } from './reporting/log-storage.js';
|
|
@@ -70,6 +70,9 @@ export interface OrchestratorSubsystems {
|
|
|
70
70
|
cacheStorage: CacheStorage | undefined;
|
|
71
71
|
sourceCache: SourceCache | undefined;
|
|
72
72
|
depCache: DepCache | undefined;
|
|
73
|
+
userCache: UserCache | undefined;
|
|
74
|
+
/** Server-side jobId -> user-cache-namespace store, written at dispatch, read by the agent-WS handler. */
|
|
75
|
+
dispatchCacheRefs: DispatchCacheRefTracker;
|
|
73
76
|
buildCoordinator: BuildCoordinator | undefined;
|
|
74
77
|
pendingBuilds: PendingBuildTracker | undefined;
|
|
75
78
|
pendingInits: PendingInitTracker;
|
|
@@ -143,6 +146,14 @@ export interface OrchestratorSubsystems {
|
|
|
143
146
|
* store. the metrics enforcement plan.
|
|
144
147
|
*/
|
|
145
148
|
agentMetricsAggregator: AgentMetricsAggregator;
|
|
149
|
+
/**
|
|
150
|
+
* Peer-side fleet collect responder. On an inbound peer.logs.collect.request,
|
|
151
|
+
* assembles this node's subtree and streams it back. Wired into every outgoing
|
|
152
|
+
* PeerClient (the incoming peer-handler is wired directly in setupCluster).
|
|
153
|
+
* Armed after the fleet runtime is built, so calls before that resolve to a
|
|
154
|
+
* no-op.
|
|
155
|
+
*/
|
|
156
|
+
fleetCollectResponder: (msg: PeerLogsCollectRequest, send: (out: PeerToPeerMessage) => boolean) => Promise<void>;
|
|
146
157
|
}
|
|
147
158
|
/**
|
|
148
159
|
* Mode-specific hooks injected by each entry point.
|