@kici-dev/orchestrator 0.12.0 → 0.14.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.
- package/dist/agent/dispatcher.d.ts +39 -0
- package/dist/agent/registry.d.ts +31 -0
- package/dist/app.d.ts +6 -0
- package/dist/cache/cache-upload-settle.d.ts +75 -0
- package/dist/cache/pending-inits.d.ts +9 -0
- package/dist/cancel/cancel-run.d.ts +22 -4
- package/dist/cancel/peer-job-cancel.d.ts +62 -0
- package/dist/cancel/sweep-stuck-cancelling.d.ts +3 -1
- package/dist/cli/api-client.d.ts +73 -0
- package/dist/cli/commands/cluster-settings.d.ts +31 -1
- package/dist/cli/commands/local-hook.d.ts +3 -2
- package/dist/cli/commands/local-trigger.d.ts +7 -0
- package/dist/cli/commands/peer.d.ts +27 -1
- package/dist/cli/commands/scaler.d.ts +40 -4
- package/dist/cli.js +2549 -972
- package/dist/cluster/cluster-settings-reader.d.ts +8 -2
- package/dist/cluster/coordinator-credential.d.ts +45 -0
- package/dist/cluster/coordinator.d.ts +52 -10
- package/dist/cluster/index.d.ts +1 -0
- package/dist/cluster/instance-heartbeat.d.ts +6 -1
- package/dist/cluster/join-token.d.ts +2 -0
- package/dist/cluster/peer-auth-coordinator.d.ts +13 -0
- package/dist/cluster/peer-client.d.ts +41 -4
- package/dist/cluster/peer-credentials.d.ts +31 -0
- package/dist/cluster/peer-forget.d.ts +138 -0
- package/dist/cluster/peer-handler.d.ts +18 -1
- package/dist/cluster/scaler-orphans-peer.d.ts +40 -0
- package/dist/cluster/settings-propagation.d.ts +85 -0
- package/dist/cluster/worker-cluster-settings.d.ts +29 -0
- package/dist/config.d.ts +12 -0
- package/dist/db/migrations/152_org_settings_cache_upload_settle_timeout.d.ts +15 -0
- package/dist/db/migrations/153_execution_runs_cancel_force.d.ts +16 -0
- package/dist/db/migrations/154_cluster_settings_firecracker_api_socket_wait.d.ts +4 -0
- package/dist/db/migrations/155_org_settings_reroute_spawn_retry.d.ts +17 -0
- package/dist/db/migrations/156_kici_events_match_outcome.d.ts +17 -0
- package/dist/db/types.d.ts +31 -0
- package/dist/events/event-router.d.ts +7 -1
- package/dist/events/event-store.d.ts +20 -3
- package/dist/events/types.d.ts +45 -0
- package/dist/helpers/windows-batch.d.ts +59 -0
- package/dist/index.js +252 -53
- package/dist/metrics/prometheus.d.ts +2 -0
- package/dist/orchestrator-core.d.ts +7 -0
- package/dist/pipeline/changed-files.d.ts +51 -0
- package/dist/pipeline/cross-source-tally.d.ts +21 -0
- package/dist/pipeline/degraded-reason.d.ts +8 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +10 -0
- package/dist/pipeline/global-eval-round.d.ts +3 -2
- package/dist/pipeline/processor.d.ts +9 -1
- package/dist/pipeline/rerun-inputs.d.ts +16 -7
- package/dist/providers/github/changed-files.d.ts +14 -4
- package/dist/providers/universal-git/changed-files.d.ts +6 -0
- package/dist/providers/universal-git/config.d.ts +4 -0
- package/dist/registration/registration-store.d.ts +10 -0
- package/dist/routes/admin-cluster-settings.d.ts +13 -1
- package/dist/routes/admin-event-inspect.d.ts +36 -0
- package/dist/routes/admin-peer-forget.d.ts +43 -0
- package/dist/routes/admin-scaler-orphans.d.ts +64 -0
- package/dist/routes/admin.d.ts +23 -0
- package/dist/scaler/backend-factory.d.ts +6 -1
- package/dist/scaler/bare-metal-backend.d.ts +17 -15
- package/dist/scaler/bare-metal-launch.d.ts +90 -0
- package/dist/scaler/container-backend.d.ts +2 -0
- package/dist/scaler/container-runtime.d.ts +9 -0
- package/dist/scaler/firecracker-backend.d.ts +181 -7
- package/dist/scaler/live-vms.d.ts +57 -0
- package/dist/scaler/manager.d.ts +138 -5
- package/dist/scaler/orphan-requests.d.ts +23 -0
- package/dist/scaler/overlay-template.d.ts +38 -0
- package/dist/scaler/scaler-state-store.d.ts +7 -0
- package/dist/scaler/spawn-errors.d.ts +17 -0
- package/dist/scaler/startup-orphan-sweep.d.ts +15 -0
- package/dist/scaler/types.d.ts +21 -2
- package/dist/scaler/vm-job-bindings.d.ts +17 -0
- package/dist/secrets/rbac.d.ts +7 -1
- package/dist/server.js +7086 -2557
- package/dist/stale-detector/workflow-deadline-detector.d.ts +7 -1
- package/dist/standalone.js +16473 -11945
- package/dist/storage/s3.d.ts +36 -0
- package/dist/worker/in-memory-execution-tracker.d.ts +5 -0
- package/dist/worker/in-memory-job-queue.d.ts +21 -1
- package/dist/worker/queued-reroute-release.d.ts +39 -0
- package/dist/worker/reroute-spawn-control.d.ts +58 -0
- package/dist/worker/spawn-retry-budget.d.ts +87 -0
- package/dist/worker-core.d.ts +11 -2
- package/dist/ws/agent-handler.d.ts +24 -3
- package/dist/ws/agent-heartbeat.d.ts +2 -2
- package/installer-image-digests.json +3 -3
- package/package.json +5 -5
- package/sbom.spdx.json +53 -53
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { exec } from "node:child_process";
|
|
|
33
33
|
* TTL is enforced lazily: expired items are deleted on access.
|
|
34
34
|
* Pre-signed URLs are generated for getUrl() with 15-minute expiry.
|
|
35
35
|
*/
|
|
36
|
+
const logger$5 = createLogger({ prefix: "s3-cache" });
|
|
36
37
|
/** Pre-signed download URL expiry in seconds (15 minutes) */
|
|
37
38
|
const PRESIGNED_URL_EXPIRY_SECONDS = 900;
|
|
38
39
|
/** Pre-signed upload URL expiry in seconds (30 minutes -- uploads take longer) */
|
|
@@ -46,6 +47,11 @@ var S3CacheStorage = class {
|
|
|
46
47
|
bucket;
|
|
47
48
|
prefix;
|
|
48
49
|
ttlMs;
|
|
50
|
+
/**
|
|
51
|
+
* False once the backend answered a conditional self-copy with
|
|
52
|
+
* `501 NotImplemented`. From then on `updateMeta` copies unconditionally.
|
|
53
|
+
*/
|
|
54
|
+
conditionalCopySupported = true;
|
|
49
55
|
constructor(options) {
|
|
50
56
|
this.bucket = options.bucket;
|
|
51
57
|
this.prefix = options.prefix;
|
|
@@ -77,6 +83,13 @@ var S3CacheStorage = class {
|
|
|
77
83
|
* Returns null if the object doesn't exist (NoSuchKey/NotFound).
|
|
78
84
|
*/
|
|
79
85
|
async readMeta(key) {
|
|
86
|
+
return (await this.readHead(key))?.meta ?? null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read an object's TTL metadata together with the ETag of the version it
|
|
90
|
+
* belongs to. Returns null if the object doesn't exist (NoSuchKey/NotFound).
|
|
91
|
+
*/
|
|
92
|
+
async readHead(key) {
|
|
80
93
|
try {
|
|
81
94
|
const head = await this.client.send(new HeadObjectCommand({
|
|
82
95
|
Bucket: this.bucket,
|
|
@@ -86,8 +99,11 @@ var S3CacheStorage = class {
|
|
|
86
99
|
const lastAccessedAt = head.Metadata?.["last-accessed-at"] ?? "";
|
|
87
100
|
if (!createdAt || !lastAccessedAt) return null;
|
|
88
101
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
meta: {
|
|
103
|
+
createdAt,
|
|
104
|
+
lastAccessedAt
|
|
105
|
+
},
|
|
106
|
+
...head.ETag && { etag: head.ETag }
|
|
91
107
|
};
|
|
92
108
|
} catch (err) {
|
|
93
109
|
if (this.isNotFoundError(err)) return null;
|
|
@@ -97,8 +113,41 @@ var S3CacheStorage = class {
|
|
|
97
113
|
/**
|
|
98
114
|
* Update metadata on an S3 object by copying it to itself.
|
|
99
115
|
* This is the standard S3 pattern for updating metadata without re-uploading data.
|
|
116
|
+
*
|
|
117
|
+
* The copy is conditional on `etag`, the version whose metadata the caller
|
|
118
|
+
* read. A self-copy is a whole-object write: without the condition, one that
|
|
119
|
+
* races a `put()` of the same key can land after it and write the older body
|
|
120
|
+
* back. Cache pointers are rewritten in place while builds publish them, so a
|
|
121
|
+
* touch-on-read racing a publish reverted the pointer to a tarball that no
|
|
122
|
+
* longer exists. `CopySourceIfMatch` pins the bytes copied to that version and
|
|
123
|
+
* `IfMatch` refuses the write unless the key still holds it.
|
|
124
|
+
*
|
|
125
|
+
* Returns false when the object was replaced since the read (`412`), or is
|
|
126
|
+
* being replaced right now (`409 ConditionalRequestConflict`): the newer
|
|
127
|
+
* write stamps metadata of its own, so there is nothing left to refresh.
|
|
128
|
+
*
|
|
129
|
+
* An S3-compatible backend that does not implement conditional copies
|
|
130
|
+
* answers `501 NotImplemented`. The copy then runs again without the
|
|
131
|
+
* condition, and later copies skip it, as on a backend that ignores the
|
|
132
|
+
* headers.
|
|
100
133
|
*/
|
|
101
|
-
async updateMeta(key, meta) {
|
|
134
|
+
async updateMeta(key, meta, etag) {
|
|
135
|
+
const condition = this.conditionalCopySupported ? etag : void 0;
|
|
136
|
+
try {
|
|
137
|
+
await this.copyOntoItself(key, meta, condition);
|
|
138
|
+
return true;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (!condition) throw err;
|
|
141
|
+
if (this.isPreconditionFailedError(err) || this.isConditionalConflictError(err)) return false;
|
|
142
|
+
if (!this.isNotImplementedError(err)) throw err;
|
|
143
|
+
this.conditionalCopySupported = false;
|
|
144
|
+
logger$5.warn("The object store does not implement conditional copies; refreshing cache metadata unconditionally", { bucket: this.bucket });
|
|
145
|
+
await this.copyOntoItself(key, meta, void 0);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Copy an object onto itself with new TTL metadata, conditional on `etag` when given. */
|
|
150
|
+
async copyOntoItself(key, meta, etag) {
|
|
102
151
|
const objectKey = this.objectKey(key);
|
|
103
152
|
await this.client.send(new CopyObjectCommand({
|
|
104
153
|
Bucket: this.bucket,
|
|
@@ -108,9 +157,30 @@ var S3CacheStorage = class {
|
|
|
108
157
|
Metadata: {
|
|
109
158
|
"created-at": meta.createdAt,
|
|
110
159
|
"last-accessed-at": meta.lastAccessedAt
|
|
160
|
+
},
|
|
161
|
+
...etag && {
|
|
162
|
+
CopySourceIfMatch: etag,
|
|
163
|
+
IfMatch: etag
|
|
111
164
|
}
|
|
112
165
|
}));
|
|
113
166
|
}
|
|
167
|
+
/** Check if an error is a failed conditional request (PreconditionFailed, 412). */
|
|
168
|
+
isPreconditionFailedError(err) {
|
|
169
|
+
return this.hasErrorShape(err, "PreconditionFailed", 412);
|
|
170
|
+
}
|
|
171
|
+
/** Check if an error is a conditional write that raced another write (ConditionalRequestConflict, 409). */
|
|
172
|
+
isConditionalConflictError(err) {
|
|
173
|
+
return this.hasErrorShape(err, "ConditionalRequestConflict", 409);
|
|
174
|
+
}
|
|
175
|
+
/** Check if an error is a request the backend does not implement (NotImplemented, 501). */
|
|
176
|
+
isNotImplementedError(err) {
|
|
177
|
+
return this.hasErrorShape(err, "NotImplemented", 501);
|
|
178
|
+
}
|
|
179
|
+
hasErrorShape(err, name, status) {
|
|
180
|
+
if (typeof err !== "object" || err === null) return false;
|
|
181
|
+
if (err.name === name) return true;
|
|
182
|
+
return err.$metadata?.httpStatusCode === status;
|
|
183
|
+
}
|
|
114
184
|
/** Check if an error is a "not found" error (NoSuchKey, NotFound, 404). */
|
|
115
185
|
isNotFoundError(err) {
|
|
116
186
|
if (typeof err !== "object" || err === null) return false;
|
|
@@ -135,8 +205,9 @@ var S3CacheStorage = class {
|
|
|
135
205
|
}).done();
|
|
136
206
|
}
|
|
137
207
|
async get(key, ttlMsOverride) {
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
208
|
+
const head = await this.readHead(key);
|
|
209
|
+
if (!head) return null;
|
|
210
|
+
const { meta, etag } = head;
|
|
140
211
|
if (this.isExpired(meta, ttlMsOverride)) {
|
|
141
212
|
await this.deleteObject(key);
|
|
142
213
|
return null;
|
|
@@ -155,7 +226,7 @@ var S3CacheStorage = class {
|
|
|
155
226
|
}
|
|
156
227
|
try {
|
|
157
228
|
meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
158
|
-
await this.updateMeta(key, meta);
|
|
229
|
+
await this.updateMeta(key, meta, etag);
|
|
159
230
|
} catch {}
|
|
160
231
|
return data;
|
|
161
232
|
}
|
|
@@ -174,10 +245,10 @@ var S3CacheStorage = class {
|
|
|
174
245
|
return existed;
|
|
175
246
|
}
|
|
176
247
|
async touch(key) {
|
|
177
|
-
const
|
|
178
|
-
if (!
|
|
179
|
-
meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
180
|
-
await this.updateMeta(key, meta);
|
|
248
|
+
const head = await this.readHead(key);
|
|
249
|
+
if (!head) return;
|
|
250
|
+
head.meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
|
+
await this.updateMeta(key, head.meta, head.etag);
|
|
181
252
|
}
|
|
182
253
|
async getUrl(key, ttlMsOverride) {
|
|
183
254
|
const meta = await this.readMeta(key);
|
|
@@ -633,7 +704,7 @@ function createCacheStorage(config) {
|
|
|
633
704
|
* Old objects are orphaned. The cache TTL is enforced lazily on access, so
|
|
634
705
|
* nothing here removes them; `kici-admin cache purge-legacy` does.
|
|
635
706
|
*/
|
|
636
|
-
const logger$
|
|
707
|
+
const logger$4 = createLogger({ prefix: "source-cache" });
|
|
637
708
|
/**
|
|
638
709
|
* Cache key for a source tarball, addressed by the tarball's OWN content hash:
|
|
639
710
|
* `source/v2/{orgId}/{sourceTarDigest}.tar.gz`.
|
|
@@ -676,25 +747,25 @@ var SourceCache = class SourceCache {
|
|
|
676
747
|
async has(orgId, contentHash) {
|
|
677
748
|
const digest = await this.resolvePointer(orgId, contentHash);
|
|
678
749
|
if (!digest) {
|
|
679
|
-
logger$
|
|
750
|
+
logger$4.debug(`has(${contentHash}): false (no pointer)`, { orgId });
|
|
680
751
|
return false;
|
|
681
752
|
}
|
|
682
753
|
const exists = await this.storage.has(sourceTarballKey(orgId, digest));
|
|
683
|
-
logger$
|
|
754
|
+
logger$4.debug(`has(${contentHash}): ${exists}`, { orgId });
|
|
684
755
|
return exists;
|
|
685
756
|
}
|
|
686
757
|
async get(orgId, contentHash) {
|
|
687
758
|
const digest = await this.resolvePointer(orgId, contentHash);
|
|
688
759
|
if (!digest) {
|
|
689
|
-
logger$
|
|
760
|
+
logger$4.debug(`get(${contentHash}): miss (no pointer)`, { orgId });
|
|
690
761
|
return null;
|
|
691
762
|
}
|
|
692
763
|
const key = sourceTarballKey(orgId, digest);
|
|
693
764
|
const data = await this.storage.get(key);
|
|
694
765
|
if (data) {
|
|
695
766
|
await this.storage.touch(key);
|
|
696
|
-
logger$
|
|
697
|
-
} else logger$
|
|
767
|
+
logger$4.debug(`get(${contentHash}): hit (${data.length} bytes)`, { orgId });
|
|
768
|
+
} else logger$4.debug(`get(${contentHash}): miss (dangling pointer)`, { orgId });
|
|
698
769
|
return data;
|
|
699
770
|
}
|
|
700
771
|
/**
|
|
@@ -710,18 +781,18 @@ var SourceCache = class SourceCache {
|
|
|
710
781
|
const pointerKey = sourcePointerKey(orgId, contentHash);
|
|
711
782
|
const digest = (await this.storage.get(pointerKey))?.toString("utf-8").trim() || null;
|
|
712
783
|
if (!digest) {
|
|
713
|
-
logger$
|
|
784
|
+
logger$4.debug(`getUrlAndDigest(${contentHash}): miss (no pointer)`, { orgId });
|
|
714
785
|
return null;
|
|
715
786
|
}
|
|
716
787
|
const key = sourceTarballKey(orgId, digest);
|
|
717
788
|
const url = await this.storage.getUrl(key);
|
|
718
789
|
if (!url) {
|
|
719
|
-
logger$
|
|
790
|
+
logger$4.debug(`getUrlAndDigest(${contentHash}): miss (dangling pointer)`, { orgId });
|
|
720
791
|
return null;
|
|
721
792
|
}
|
|
722
793
|
await this.storage.touch(key);
|
|
723
794
|
await this.storage.touch(pointerKey);
|
|
724
|
-
logger$
|
|
795
|
+
logger$4.debug(`getUrlAndDigest(${contentHash}): hit`, { orgId });
|
|
725
796
|
return {
|
|
726
797
|
url,
|
|
727
798
|
digest
|
|
@@ -747,7 +818,7 @@ var SourceCache = class SourceCache {
|
|
|
747
818
|
*/
|
|
748
819
|
async publishPointer(orgId, contentHash, sourceTarDigest) {
|
|
749
820
|
await this.storage.put(sourcePointerKey(orgId, contentHash), sourceTarDigest);
|
|
750
|
-
logger$
|
|
821
|
+
logger$4.info(`publishPointer(${contentHash}) -> ${sourceTarDigest.slice(0, 12)}`, { orgId });
|
|
751
822
|
}
|
|
752
823
|
/** Store a tarball and the pointer that names it. */
|
|
753
824
|
async store(orgId, contentHash, tarball) {
|
|
@@ -755,7 +826,7 @@ var SourceCache = class SourceCache {
|
|
|
755
826
|
const digest = SourceCache.computeDigest(data);
|
|
756
827
|
await this.storage.put(sourceTarballKey(orgId, digest), data);
|
|
757
828
|
await this.storage.put(sourcePointerKey(orgId, contentHash), digest);
|
|
758
|
-
logger$
|
|
829
|
+
logger$4.info(`store(${contentHash}): stored (${data.length} bytes)`, {
|
|
759
830
|
orgId,
|
|
760
831
|
digest: digest.slice(0, 12)
|
|
761
832
|
});
|
|
@@ -769,7 +840,7 @@ var SourceCache = class SourceCache {
|
|
|
769
840
|
const digest = await this.resolvePointer(orgId, contentHash);
|
|
770
841
|
await this.storage.delete(sourcePointerKey(orgId, contentHash));
|
|
771
842
|
const removed = digest ? await this.storage.delete(sourceTarballKey(orgId, digest)) : false;
|
|
772
|
-
logger$
|
|
843
|
+
logger$4.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`, { orgId });
|
|
773
844
|
return removed;
|
|
774
845
|
}
|
|
775
846
|
};
|
|
@@ -786,7 +857,7 @@ var SourceCache = class SourceCache {
|
|
|
786
857
|
* Keys: `deps/{platform}-{arch}/{depsHash}.tar.gz` (immutable) and
|
|
787
858
|
* `deps/{platform}-{arch}/{lockfileHash}.hash` (the pointer).
|
|
788
859
|
*/
|
|
789
|
-
const logger$
|
|
860
|
+
const logger$3 = createLogger({ prefix: "dep-cache" });
|
|
790
861
|
/** Default max tarball size: 500MB */
|
|
791
862
|
const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
|
|
792
863
|
/** Default dependency-cache entry TTL: 30 days. */
|
|
@@ -867,14 +938,14 @@ var DepCache = class DepCache {
|
|
|
867
938
|
const ttlMsOverride = await this.resolveTtlMsOverride();
|
|
868
939
|
const depsHash = await this.resolvePointer(lockfileHash, platform, arch, ttlMsOverride, siblingsDigest);
|
|
869
940
|
if (!depsHash) {
|
|
870
|
-
logger$
|
|
941
|
+
logger$3.debug(`has(${lockfileHash}): false (no pointer)`, {
|
|
871
942
|
platform,
|
|
872
943
|
arch
|
|
873
944
|
});
|
|
874
945
|
return false;
|
|
875
946
|
}
|
|
876
947
|
const exists = await this.storage.has(depTarballKey(depsHash, platform, arch), ttlMsOverride);
|
|
877
|
-
logger$
|
|
948
|
+
logger$3.debug(`has(${lockfileHash}): ${exists}`, {
|
|
878
949
|
platform,
|
|
879
950
|
arch
|
|
880
951
|
});
|
|
@@ -898,7 +969,7 @@ var DepCache = class DepCache {
|
|
|
898
969
|
const pointerKey = depPointerKey(lockfileHash, platform, arch, siblingsDigest);
|
|
899
970
|
const hash = (await this.storage.get(pointerKey, ttlMsOverride))?.toString("utf-8").trim() || void 0;
|
|
900
971
|
if (!hash) {
|
|
901
|
-
logger$
|
|
972
|
+
logger$3.debug(`getUrlAndHash(${lockfileHash}): miss (no pointer)`, {
|
|
902
973
|
platform,
|
|
903
974
|
arch
|
|
904
975
|
});
|
|
@@ -907,7 +978,7 @@ var DepCache = class DepCache {
|
|
|
907
978
|
const key = depTarballKey(hash, platform, arch);
|
|
908
979
|
const url = await this.storage.getUrl(key, ttlMsOverride);
|
|
909
980
|
if (!url) {
|
|
910
|
-
logger$
|
|
981
|
+
logger$3.debug(`getUrlAndHash(${lockfileHash}): miss (dangling pointer)`, {
|
|
911
982
|
platform,
|
|
912
983
|
arch
|
|
913
984
|
});
|
|
@@ -915,7 +986,7 @@ var DepCache = class DepCache {
|
|
|
915
986
|
}
|
|
916
987
|
await this.storage.touch(key);
|
|
917
988
|
await this.storage.touch(pointerKey);
|
|
918
|
-
logger$
|
|
989
|
+
logger$3.debug(`getUrlAndHash(${lockfileHash}): hit`, {
|
|
919
990
|
platform,
|
|
920
991
|
arch,
|
|
921
992
|
hasHash: true
|
|
@@ -939,7 +1010,7 @@ var DepCache = class DepCache {
|
|
|
939
1010
|
*/
|
|
940
1011
|
async publishPointer(lockfileHash, platform, arch, depsHash, siblingsDigest) {
|
|
941
1012
|
await this.storage.put(depPointerKey(lockfileHash, platform, arch, siblingsDigest), depsHash);
|
|
942
|
-
logger$
|
|
1013
|
+
logger$3.info(`publishPointer(${lockfileHash}) -> ${depsHash.slice(0, 12)}`, {
|
|
943
1014
|
platform,
|
|
944
1015
|
arch
|
|
945
1016
|
});
|
|
@@ -954,7 +1025,7 @@ var DepCache = class DepCache {
|
|
|
954
1025
|
const depsHash = DepCache.computeHash(tarballData);
|
|
955
1026
|
await this.storage.put(depTarballKey(depsHash, platform, arch), tarballData);
|
|
956
1027
|
await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
|
|
957
|
-
logger$
|
|
1028
|
+
logger$3.info(`store: ${tarballData.length} bytes`, {
|
|
958
1029
|
lockfileHash,
|
|
959
1030
|
platform,
|
|
960
1031
|
arch,
|
|
@@ -973,7 +1044,7 @@ var DepCache = class DepCache {
|
|
|
973
1044
|
const depsHash = await this.resolvePointer(lockfileHash, platform, arch, await this.resolveTtlMsOverride());
|
|
974
1045
|
await this.storage.delete(depPointerKey(lockfileHash, platform, arch));
|
|
975
1046
|
const removed = depsHash ? await this.storage.delete(depTarballKey(depsHash, platform, arch)) : false;
|
|
976
|
-
logger$
|
|
1047
|
+
logger$3.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
|
|
977
1048
|
platform,
|
|
978
1049
|
arch
|
|
979
1050
|
});
|
|
@@ -1017,7 +1088,7 @@ function keyDiscriminator(exact) {
|
|
|
1017
1088
|
* hash and size accounting outside the tarball's own (presigned, metadata-less)
|
|
1018
1089
|
* upload.
|
|
1019
1090
|
*/
|
|
1020
|
-
const logger$
|
|
1091
|
+
const logger$2 = createLogger({ prefix: "user-cache" });
|
|
1021
1092
|
/**
|
|
1022
1093
|
* Cluster-wide default quota: 5 GiB. Serves as the fallback when an org has no
|
|
1023
1094
|
* per-org override in `org_settings.user_cache_quota_bytes`. The cluster-wide
|
|
@@ -1165,7 +1236,7 @@ var UserCache = class {
|
|
|
1165
1236
|
try {
|
|
1166
1237
|
limits = await this.orgLimitsReader(org);
|
|
1167
1238
|
} catch (err) {
|
|
1168
|
-
logger$
|
|
1239
|
+
logger$2.warn("user-cache org-limits lookup failed — using cluster defaults", {
|
|
1169
1240
|
org,
|
|
1170
1241
|
error: err instanceof Error ? err.message : String(err)
|
|
1171
1242
|
});
|
|
@@ -1222,7 +1293,7 @@ var UserCache = class {
|
|
|
1222
1293
|
async beginSave(ref) {
|
|
1223
1294
|
const final = userCacheEntryKey(ref, ref.key);
|
|
1224
1295
|
if (await this.storage.has(final)) {
|
|
1225
|
-
logger$
|
|
1296
|
+
logger$2.info("user-cache save skipped (immutable key exists)", { key: ref.key });
|
|
1226
1297
|
return { skip: true };
|
|
1227
1298
|
}
|
|
1228
1299
|
const tempKey = `${writePrefix(ref)}${TEMP_UPLOAD_STEM}${randomUUID()}${TAR_SUFFIX}`;
|
|
@@ -1243,7 +1314,7 @@ var UserCache = class {
|
|
|
1243
1314
|
await this.storage.initMeta(final);
|
|
1244
1315
|
await this.storage.put(`${final}.hash`, ref.tarHash);
|
|
1245
1316
|
await this.storage.put(`${final}.size`, String(ref.sizeBytes));
|
|
1246
|
-
logger$
|
|
1317
|
+
logger$2.info("user-cache entry committed", {
|
|
1247
1318
|
key: ref.key,
|
|
1248
1319
|
sizeBytes: ref.sizeBytes
|
|
1249
1320
|
});
|
|
@@ -1293,7 +1364,7 @@ var UserCache = class {
|
|
|
1293
1364
|
await this.storage.delete(`${key}.hash`);
|
|
1294
1365
|
await this.storage.delete(`${key}.size`);
|
|
1295
1366
|
total -= size;
|
|
1296
|
-
logger$
|
|
1367
|
+
logger$2.info("user-cache eviction (org over quota)", {
|
|
1297
1368
|
org: ref.org,
|
|
1298
1369
|
key,
|
|
1299
1370
|
freedBytes: size
|
|
@@ -1301,18 +1372,12 @@ var UserCache = class {
|
|
|
1301
1372
|
}
|
|
1302
1373
|
}
|
|
1303
1374
|
};
|
|
1304
|
-
//#endregion
|
|
1305
|
-
//#region src/cluster/peer-credentials.ts
|
|
1306
1375
|
/**
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
* 1. DB CRUD (PeerCredentialStore) — coordinator stores credentials for all peers
|
|
1311
|
-
* 2. File I/O (readCredentialFile/writeCredentialFile) — workers persist their
|
|
1312
|
-
* credential locally for reconnection after restart
|
|
1313
|
-
*
|
|
1314
|
-
* Credential files are written with 0600 permissions to protect the secret.
|
|
1376
|
+
* `metadata` key set when a coordinator retires the self-issued credential its
|
|
1377
|
+
* previous run left behind. An operator revoke never sets it, which is how a
|
|
1378
|
+
* retirement is told apart from a revoke.
|
|
1315
1379
|
*/
|
|
1380
|
+
const RETIRED_BY_INSTANCE_KEY = "retiredByInstance";
|
|
1316
1381
|
/** Default credential expiry: 90 days. */
|
|
1317
1382
|
const DEFAULT_EXPIRY_DAYS = 90;
|
|
1318
1383
|
/**
|
|
@@ -1349,7 +1414,8 @@ var PeerCredentialStore = class {
|
|
|
1349
1414
|
role: opts.role,
|
|
1350
1415
|
routing_keys: sql`${sql.val(opts.routingKeys)}`,
|
|
1351
1416
|
source_token_hash: opts.sourceTokenHash ?? null,
|
|
1352
|
-
expires_at: expiresAt
|
|
1417
|
+
expires_at: expiresAt,
|
|
1418
|
+
...opts.metadata && { metadata: sql`${JSON.stringify(opts.metadata)}::jsonb` }
|
|
1353
1419
|
}).execute();
|
|
1354
1420
|
return Number(revokeResult?.numUpdatedRows ?? 0n);
|
|
1355
1421
|
});
|
|
@@ -1376,6 +1442,33 @@ var PeerCredentialStore = class {
|
|
|
1376
1442
|
return row ? mapRow(row) : null;
|
|
1377
1443
|
}
|
|
1378
1444
|
/**
|
|
1445
|
+
* The credential row for `instanceId` that nobody revoked, expired or not.
|
|
1446
|
+
* `peer_credentials_active_uniq` allows at most one.
|
|
1447
|
+
*/
|
|
1448
|
+
async findUnrevokedByInstanceId(instanceId) {
|
|
1449
|
+
const row = await this.db.selectFrom("peer_credentials").selectAll().where("instance_id", "=", instanceId).where("revoked_at", "is", null).executeTakeFirst();
|
|
1450
|
+
return row ? mapRow(row) : null;
|
|
1451
|
+
}
|
|
1452
|
+
/** The most recently revoked credential row for `instanceId`, or null. */
|
|
1453
|
+
async findLatestRevokedByInstanceId(instanceId) {
|
|
1454
|
+
const row = await this.db.selectFrom("peer_credentials").selectAll().where("instance_id", "=", instanceId).where("revoked_at", "is not", null).orderBy("revoked_at", "desc").executeTakeFirst();
|
|
1455
|
+
return row ? mapRow(row) : null;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Revoke `instanceId`'s unrevoked, self-issued row whose hash is
|
|
1459
|
+
* `credentialHash`, recording who retired it. Every condition sits in the
|
|
1460
|
+
* WHERE clause, so a row that changed meanwhile is left alone.
|
|
1461
|
+
* Returns whether a row was retired.
|
|
1462
|
+
*/
|
|
1463
|
+
async retireSelfIssued(opts) {
|
|
1464
|
+
const marker = JSON.stringify({ [RETIRED_BY_INSTANCE_KEY]: opts.retiredByInstance });
|
|
1465
|
+
const result = await this.db.updateTable("peer_credentials").set({
|
|
1466
|
+
revoked_at: /* @__PURE__ */ new Date(),
|
|
1467
|
+
metadata: sql`COALESCE(metadata, '{}'::jsonb) || ${marker}::jsonb`
|
|
1468
|
+
}).where("instance_id", "=", opts.instanceId).where("credential_hash", "=", opts.credentialHash).where("revoked_at", "is", null).where(sql`metadata->>'issuance'`, "=", "self").executeTakeFirst();
|
|
1469
|
+
return Number(result?.numUpdatedRows ?? 0n) > 0;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1379
1472
|
* Update the last_seen_at timestamp for a credential.
|
|
1380
1473
|
* Optionally records which coordinator validated it.
|
|
1381
1474
|
*/
|
|
@@ -1420,7 +1513,8 @@ function mapRow(row) {
|
|
|
1420
1513
|
lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at) : null,
|
|
1421
1514
|
lastValidatedBy: row.last_validated_by ?? null,
|
|
1422
1515
|
expiresAt: new Date(row.expires_at),
|
|
1423
|
-
revokedAt: row.revoked_at ? new Date(row.revoked_at) : null
|
|
1516
|
+
revokedAt: row.revoked_at ? new Date(row.revoked_at) : null,
|
|
1517
|
+
metadata: row.metadata ?? {}
|
|
1424
1518
|
};
|
|
1425
1519
|
}
|
|
1426
1520
|
/**
|
|
@@ -1514,6 +1608,8 @@ function runDetached(logger, task, work, context = {}) {
|
|
|
1514
1608
|
}
|
|
1515
1609
|
Promise.resolve(outcome).catch(fail);
|
|
1516
1610
|
}
|
|
1611
|
+
createLogger({ prefix: "instance-heartbeat" });
|
|
1612
|
+
createLogger({ prefix: "coordinator-credential" });
|
|
1517
1613
|
//#endregion
|
|
1518
1614
|
//#region src/cluster/peer-auth-coordinator.ts
|
|
1519
1615
|
/**
|
|
@@ -1526,6 +1622,10 @@ function runDetached(logger, task, work, context = {}) {
|
|
|
1526
1622
|
* rejection — a credential revocation cascade. This coordinator serializes all
|
|
1527
1623
|
* file access through one in-process mutex so only one peer-client token-joins
|
|
1528
1624
|
* per storm, and it never deletes a credential file a sibling has refreshed.
|
|
1625
|
+
*
|
|
1626
|
+
* A coordinator without a join token also wires a `selfIssue` callback. When a
|
|
1627
|
+
* decision finds no credential and no token, the callback issues the
|
|
1628
|
+
* coordinator its own credential, once per storm, under the same mutex.
|
|
1529
1629
|
*/
|
|
1530
1630
|
const logger = createLogger({ prefix: "peer-auth-coordinator" });
|
|
1531
1631
|
/** How long a waiting peer-client awaits an in-flight sibling token-join. */
|
|
@@ -1546,6 +1646,11 @@ var PeerAuthCoordinator = class {
|
|
|
1546
1646
|
instanceId;
|
|
1547
1647
|
joinToken;
|
|
1548
1648
|
joinWaitTimeoutMs;
|
|
1649
|
+
selfIssue;
|
|
1650
|
+
/** The issuance in progress; concurrent sibling clients share it. */
|
|
1651
|
+
selfIssueInFlight = null;
|
|
1652
|
+
/** An operator revoked this coordinator's credential: never issue again in this process. */
|
|
1653
|
+
selfIssueRevoked = false;
|
|
1549
1654
|
/** Promise-chain mutex tail; every file op awaits the prior one. */
|
|
1550
1655
|
lock = Promise.resolve();
|
|
1551
1656
|
/** Set while one peer-client is mid token-join; siblings await it. */
|
|
@@ -1555,6 +1660,7 @@ var PeerAuthCoordinator = class {
|
|
|
1555
1660
|
this.instanceId = opts.instanceId;
|
|
1556
1661
|
this.joinToken = opts.joinToken;
|
|
1557
1662
|
this.joinWaitTimeoutMs = opts.joinWaitTimeoutMs ?? DEFAULT_JOIN_WAIT_TIMEOUT_MS;
|
|
1663
|
+
this.selfIssue = opts.selfIssue;
|
|
1558
1664
|
}
|
|
1559
1665
|
/** Run `fn` exclusively against the credential file. */
|
|
1560
1666
|
async withLock(fn) {
|
|
@@ -1563,10 +1669,21 @@ var PeerAuthCoordinator = class {
|
|
|
1563
1669
|
return run;
|
|
1564
1670
|
}
|
|
1565
1671
|
async readValidCredential() {
|
|
1566
|
-
|
|
1672
|
+
let cred;
|
|
1673
|
+
try {
|
|
1674
|
+
cred = await readCredentialFile(this.credentialFile);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
logger.warn("Ignoring unreadable peer credential file", {
|
|
1677
|
+
instanceId: this.instanceId,
|
|
1678
|
+
path: this.credentialFile,
|
|
1679
|
+
error: toErrorMessage(err)
|
|
1680
|
+
});
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1567
1683
|
return cred && cred.instanceId === this.instanceId ? cred : null;
|
|
1568
1684
|
}
|
|
1569
1685
|
async decideAuth() {
|
|
1686
|
+
let selfIssueTried = false;
|
|
1570
1687
|
for (let i = 0; i < MAX_DECIDE_ITERATIONS; i++) {
|
|
1571
1688
|
const decision = await this.withLock(async () => {
|
|
1572
1689
|
const cred = await this.readValidCredential();
|
|
@@ -1584,10 +1701,19 @@ var PeerAuthCoordinator = class {
|
|
|
1584
1701
|
complete: this.makeComplete(join)
|
|
1585
1702
|
};
|
|
1586
1703
|
}
|
|
1704
|
+
if (this.selfIssue && !selfIssueTried && !this.selfIssueRevoked) return "self-issue";
|
|
1587
1705
|
return { mode: "no-auth" };
|
|
1588
1706
|
});
|
|
1589
|
-
if (decision
|
|
1590
|
-
|
|
1707
|
+
if (decision === "await-join") {
|
|
1708
|
+
await this.awaitInFlightJoin();
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (decision === "self-issue") {
|
|
1712
|
+
selfIssueTried = true;
|
|
1713
|
+
await this.runSelfIssue();
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
return decision;
|
|
1591
1717
|
}
|
|
1592
1718
|
if (this.joinToken) {
|
|
1593
1719
|
const join = deferred();
|
|
@@ -1600,6 +1726,18 @@ var PeerAuthCoordinator = class {
|
|
|
1600
1726
|
}
|
|
1601
1727
|
return { mode: "no-auth" };
|
|
1602
1728
|
}
|
|
1729
|
+
/** Run the issuer under the mutex, once for every concurrent caller. */
|
|
1730
|
+
runSelfIssue() {
|
|
1731
|
+
const selfIssue = this.selfIssue;
|
|
1732
|
+
if (!selfIssue) return Promise.resolve("failed");
|
|
1733
|
+
this.selfIssueInFlight ??= this.withLock(selfIssue).then((outcome) => {
|
|
1734
|
+
if (outcome === "revoked") this.selfIssueRevoked = true;
|
|
1735
|
+
return outcome;
|
|
1736
|
+
}).finally(() => {
|
|
1737
|
+
this.selfIssueInFlight = null;
|
|
1738
|
+
});
|
|
1739
|
+
return this.selfIssueInFlight;
|
|
1740
|
+
}
|
|
1603
1741
|
makeComplete(join) {
|
|
1604
1742
|
return (issued) => {
|
|
1605
1743
|
runDetached(logger, "Peer token-join completion", () => this.withLock(async () => {
|
|
@@ -1668,7 +1806,8 @@ var PeerAuthCoordinator = class {
|
|
|
1668
1806
|
const joinTokenLogger = createLogger({ prefix: "join-token" });
|
|
1669
1807
|
const TOKEN_PREFIX = "kici_join_v1";
|
|
1670
1808
|
const DEFAULT_EXPIRY_MS = 36e5;
|
|
1671
|
-
const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used.
|
|
1809
|
+
const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Create a new token with: kici-admin peer create-token";
|
|
1810
|
+
const TOKEN_EXPIRED_MESSAGE = "Join token has expired. Create a new token with: kici-admin peer create-token";
|
|
1672
1811
|
var JoinTokenManager = class {
|
|
1673
1812
|
deps;
|
|
1674
1813
|
constructor(deps) {
|
|
@@ -1753,7 +1892,7 @@ var JoinTokenManager = class {
|
|
|
1753
1892
|
}
|
|
1754
1893
|
const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
|
|
1755
1894
|
if (!row) throw new Error("Invalid join token");
|
|
1756
|
-
if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error(
|
|
1895
|
+
if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error(TOKEN_EXPIRED_MESSAGE);
|
|
1757
1896
|
if (row.consumed_at) {
|
|
1758
1897
|
if (row.consumed_by_instance === peerInstanceId) {
|
|
1759
1898
|
joinTokenLogger.info("Re-validated join token for returning instance", {
|
|
@@ -2721,6 +2860,51 @@ var AdminApiClient = class {
|
|
|
2721
2860
|
async drainStatus() {
|
|
2722
2861
|
return this.request("GET", "/api/v1/admin/orchestrator/drain");
|
|
2723
2862
|
}
|
|
2863
|
+
/** Forget a departed peer on this coordinator and every connected sibling. */
|
|
2864
|
+
/**
|
|
2865
|
+
* Forget a departed peer on this coordinator and every connected sibling. A
|
|
2866
|
+
* refusal that only needs the backstop acknowledgement comes back as a body
|
|
2867
|
+
* with `acknowledgementRequired`, so the caller can ask and retry; every
|
|
2868
|
+
* other failure throws.
|
|
2869
|
+
*/
|
|
2870
|
+
async forgetPeer(body) {
|
|
2871
|
+
const res = await fetchAdminApi(`${this.baseUrl}/api/v1/admin/peers/forget`, {
|
|
2872
|
+
method: "POST",
|
|
2873
|
+
headers: {
|
|
2874
|
+
Authorization: `Bearer ${this.token}`,
|
|
2875
|
+
"Content-Type": "application/json"
|
|
2876
|
+
},
|
|
2877
|
+
body: JSON.stringify(body)
|
|
2878
|
+
}, this.baseUrl);
|
|
2879
|
+
const text = await res.text();
|
|
2880
|
+
let parsed;
|
|
2881
|
+
try {
|
|
2882
|
+
parsed = JSON.parse(text);
|
|
2883
|
+
} catch {
|
|
2884
|
+
parsed = void 0;
|
|
2885
|
+
}
|
|
2886
|
+
const json = parsed ?? {};
|
|
2887
|
+
if (res.ok) return json;
|
|
2888
|
+
if (res.status === 409 && json.acknowledgementRequired === true) return {
|
|
2889
|
+
instanceId: body.instanceId,
|
|
2890
|
+
results: json.results ?? [],
|
|
2891
|
+
acknowledgementRequired: true,
|
|
2892
|
+
...json.error ? { error: json.error } : {}
|
|
2893
|
+
};
|
|
2894
|
+
throw new Error(`HTTP ${res.status}: ${json.error ?? text}`);
|
|
2895
|
+
}
|
|
2896
|
+
/** List the live Firecracker VMs on this coordinator, or on the `target` peer. */
|
|
2897
|
+
async listScalerOrphans(opts = {}) {
|
|
2898
|
+
const params = new URLSearchParams();
|
|
2899
|
+
if (opts.target !== void 0) params.set("target", opts.target);
|
|
2900
|
+
if (opts.timeoutMs !== void 0) params.set("timeoutMs", String(opts.timeoutMs));
|
|
2901
|
+
const query = params.toString();
|
|
2902
|
+
return this.request("GET", `/api/v1/admin/scaler/orphans${query ? `?${query}` : ""}`);
|
|
2903
|
+
}
|
|
2904
|
+
/** Stop the named VMs on this coordinator, or on the `target` peer. */
|
|
2905
|
+
async stopScalerOrphans(body) {
|
|
2906
|
+
return this.request("POST", "/api/v1/admin/scaler/orphans/stop", body);
|
|
2907
|
+
}
|
|
2724
2908
|
async createApiKey(opts) {
|
|
2725
2909
|
return this.request("POST", "/api/v1/api-keys", opts);
|
|
2726
2910
|
}
|
|
@@ -2847,6 +3031,21 @@ var AdminApiClient = class {
|
|
|
2847
3031
|
if (opts.routingKey) params.set("routingKey", opts.routingKey);
|
|
2848
3032
|
return this.request("GET", `/api/v1/admin/event-log/${encodeURIComponent(deliveryId)}?${params}`);
|
|
2849
3033
|
}
|
|
3034
|
+
/** List internal events, newest first (`GET /api/v1/admin/events`). */
|
|
3035
|
+
async listEvents(opts) {
|
|
3036
|
+
const params = new URLSearchParams();
|
|
3037
|
+
if (opts?.name) params.set("name", opts.name);
|
|
3038
|
+
if (opts?.outcome) params.set("outcome", opts.outcome);
|
|
3039
|
+
if (opts?.since) params.set("since", opts.since);
|
|
3040
|
+
if (opts?.before) params.set("before", opts.before);
|
|
3041
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
3042
|
+
const qs = params.toString();
|
|
3043
|
+
return this.request("GET", `/api/v1/admin/events${qs ? `?${qs}` : ""}`);
|
|
3044
|
+
}
|
|
3045
|
+
/** One internal event with its redacted payload and dispatched runs. */
|
|
3046
|
+
async getEvent(id) {
|
|
3047
|
+
return this.request("GET", `/api/v1/admin/events/${encodeURIComponent(id)}`);
|
|
3048
|
+
}
|
|
2850
3049
|
async listAccessLog(opts) {
|
|
2851
3050
|
const params = new URLSearchParams();
|
|
2852
3051
|
if (opts?.orgId) params.set("orgId", opts.orgId);
|
|
@@ -539,6 +539,8 @@ export declare const eventRetryTotal: import("@opentelemetry/api").Counter<impor
|
|
|
539
539
|
* - reason: why the event was DLQ'd (exhausted_retries)
|
|
540
540
|
*/
|
|
541
541
|
export declare const eventDlqTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
542
|
+
/** Record a reserved internal event that matched no subscriber. */
|
|
543
|
+
export declare function incEventUnmatched(eventName: string, reason: string): void;
|
|
542
544
|
/**
|
|
543
545
|
* Startup catch-up scans that threw. Emits no labels.
|
|
544
546
|
*
|
|
@@ -41,6 +41,7 @@ import { ArtifactStore } from './artifacts/artifact-store.js';
|
|
|
41
41
|
import { CheckRunReporter } from './reporting/check-run-reporter.js';
|
|
42
42
|
import type { ProcessingDeps } from './pipeline/processor.js';
|
|
43
43
|
import type { InvokeGateDeps, SummonArgs, SummonedRun } from './pipeline/invoke-gate.js';
|
|
44
|
+
import { type PeerForgetRequestHandler } from './cluster/peer-forget.js';
|
|
44
45
|
import { StepLogBuffer } from './reporting/step-log-buffer.js';
|
|
45
46
|
import { type LogStorage } from './reporting/log-storage.js';
|
|
46
47
|
import { ExecutionTracker, type ExecutionTrackerDeps } from './reporting/execution-tracker.js';
|
|
@@ -167,6 +168,12 @@ export interface OrchestratorSubsystems {
|
|
|
167
168
|
orphanRecovery: OrphanRecovery;
|
|
168
169
|
peerHandler: ReturnType<typeof createPeerHandler>;
|
|
169
170
|
peerClients: Map<string, PeerClient>;
|
|
171
|
+
/**
|
|
172
|
+
* Answers a `kici-admin peer forget` a sibling coordinator fanned out. Wired
|
|
173
|
+
* into every outgoing PeerClient (the incoming peer-handler is wired directly
|
|
174
|
+
* in setupCluster).
|
|
175
|
+
*/
|
|
176
|
+
answerPeerForgetRequest: PeerForgetRequestHandler;
|
|
170
177
|
getLocalInventory: () => Omit<PeerHeartbeat, 'type'>;
|
|
171
178
|
broadcastHeartbeatToAllPeers: () => void;
|
|
172
179
|
broadcastAgentTokenRevoke: (tokenId: string) => void;
|