@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.
Files changed (90) hide show
  1. package/dist/agent/dispatcher.d.ts +39 -0
  2. package/dist/agent/registry.d.ts +31 -0
  3. package/dist/app.d.ts +6 -0
  4. package/dist/cache/cache-upload-settle.d.ts +75 -0
  5. package/dist/cache/pending-inits.d.ts +9 -0
  6. package/dist/cancel/cancel-run.d.ts +22 -4
  7. package/dist/cancel/peer-job-cancel.d.ts +62 -0
  8. package/dist/cancel/sweep-stuck-cancelling.d.ts +3 -1
  9. package/dist/cli/api-client.d.ts +73 -0
  10. package/dist/cli/commands/cluster-settings.d.ts +31 -1
  11. package/dist/cli/commands/local-hook.d.ts +3 -2
  12. package/dist/cli/commands/local-trigger.d.ts +7 -0
  13. package/dist/cli/commands/peer.d.ts +27 -1
  14. package/dist/cli/commands/scaler.d.ts +40 -4
  15. package/dist/cli.js +2549 -972
  16. package/dist/cluster/cluster-settings-reader.d.ts +8 -2
  17. package/dist/cluster/coordinator-credential.d.ts +45 -0
  18. package/dist/cluster/coordinator.d.ts +52 -10
  19. package/dist/cluster/index.d.ts +1 -0
  20. package/dist/cluster/instance-heartbeat.d.ts +6 -1
  21. package/dist/cluster/join-token.d.ts +2 -0
  22. package/dist/cluster/peer-auth-coordinator.d.ts +13 -0
  23. package/dist/cluster/peer-client.d.ts +41 -4
  24. package/dist/cluster/peer-credentials.d.ts +31 -0
  25. package/dist/cluster/peer-forget.d.ts +138 -0
  26. package/dist/cluster/peer-handler.d.ts +18 -1
  27. package/dist/cluster/scaler-orphans-peer.d.ts +40 -0
  28. package/dist/cluster/settings-propagation.d.ts +85 -0
  29. package/dist/cluster/worker-cluster-settings.d.ts +29 -0
  30. package/dist/config.d.ts +12 -0
  31. package/dist/db/migrations/152_org_settings_cache_upload_settle_timeout.d.ts +15 -0
  32. package/dist/db/migrations/153_execution_runs_cancel_force.d.ts +16 -0
  33. package/dist/db/migrations/154_cluster_settings_firecracker_api_socket_wait.d.ts +4 -0
  34. package/dist/db/migrations/155_org_settings_reroute_spawn_retry.d.ts +17 -0
  35. package/dist/db/migrations/156_kici_events_match_outcome.d.ts +17 -0
  36. package/dist/db/types.d.ts +31 -0
  37. package/dist/events/event-router.d.ts +7 -1
  38. package/dist/events/event-store.d.ts +20 -3
  39. package/dist/events/types.d.ts +45 -0
  40. package/dist/helpers/windows-batch.d.ts +59 -0
  41. package/dist/index.js +252 -53
  42. package/dist/metrics/prometheus.d.ts +2 -0
  43. package/dist/orchestrator-core.d.ts +7 -0
  44. package/dist/pipeline/changed-files.d.ts +51 -0
  45. package/dist/pipeline/cross-source-tally.d.ts +21 -0
  46. package/dist/pipeline/degraded-reason.d.ts +8 -0
  47. package/dist/pipeline/dispatch-matched-workflow.d.ts +10 -0
  48. package/dist/pipeline/global-eval-round.d.ts +3 -2
  49. package/dist/pipeline/processor.d.ts +9 -1
  50. package/dist/pipeline/rerun-inputs.d.ts +16 -7
  51. package/dist/providers/github/changed-files.d.ts +14 -4
  52. package/dist/providers/universal-git/changed-files.d.ts +6 -0
  53. package/dist/providers/universal-git/config.d.ts +4 -0
  54. package/dist/registration/registration-store.d.ts +10 -0
  55. package/dist/routes/admin-cluster-settings.d.ts +13 -1
  56. package/dist/routes/admin-event-inspect.d.ts +36 -0
  57. package/dist/routes/admin-peer-forget.d.ts +43 -0
  58. package/dist/routes/admin-scaler-orphans.d.ts +64 -0
  59. package/dist/routes/admin.d.ts +23 -0
  60. package/dist/scaler/backend-factory.d.ts +6 -1
  61. package/dist/scaler/bare-metal-backend.d.ts +17 -15
  62. package/dist/scaler/bare-metal-launch.d.ts +90 -0
  63. package/dist/scaler/container-backend.d.ts +2 -0
  64. package/dist/scaler/container-runtime.d.ts +9 -0
  65. package/dist/scaler/firecracker-backend.d.ts +181 -7
  66. package/dist/scaler/live-vms.d.ts +57 -0
  67. package/dist/scaler/manager.d.ts +138 -5
  68. package/dist/scaler/orphan-requests.d.ts +23 -0
  69. package/dist/scaler/overlay-template.d.ts +38 -0
  70. package/dist/scaler/scaler-state-store.d.ts +7 -0
  71. package/dist/scaler/spawn-errors.d.ts +17 -0
  72. package/dist/scaler/startup-orphan-sweep.d.ts +15 -0
  73. package/dist/scaler/types.d.ts +21 -2
  74. package/dist/scaler/vm-job-bindings.d.ts +17 -0
  75. package/dist/secrets/rbac.d.ts +7 -1
  76. package/dist/server.js +7086 -2557
  77. package/dist/stale-detector/workflow-deadline-detector.d.ts +7 -1
  78. package/dist/standalone.js +16473 -11945
  79. package/dist/storage/s3.d.ts +36 -0
  80. package/dist/worker/in-memory-execution-tracker.d.ts +5 -0
  81. package/dist/worker/in-memory-job-queue.d.ts +21 -1
  82. package/dist/worker/queued-reroute-release.d.ts +39 -0
  83. package/dist/worker/reroute-spawn-control.d.ts +58 -0
  84. package/dist/worker/spawn-retry-budget.d.ts +87 -0
  85. package/dist/worker-core.d.ts +11 -2
  86. package/dist/ws/agent-handler.d.ts +24 -3
  87. package/dist/ws/agent-heartbeat.d.ts +2 -2
  88. package/installer-image-digests.json +3 -3
  89. package/package.json +5 -5
  90. package/sbom.spdx.json +53 -53
@@ -1,7 +1,7 @@
1
1
  import type { Kysely } from 'kysely';
2
- import type { Database } from '../db/types.js';
2
+ import type { Database, ClusterSettings } from '../db/types.js';
3
3
  /** Numeric columns on cluster_settings readable via {@link ClusterSettingsReader}. */
4
- export type ClusterNumberColumn = 'max_github_payload_bytes' | 'event_log_max_payload_bytes' | 'lock_file_max_bytes' | 'webhook_dedup_ttl_ms' | 'event_router_event_ttl_seconds' | 'event_router_max_dispatch_attempts' | 'queue_max_depth' | 'reroute_flap_grace_ms' | 'max_fanout_hosts' | 'event_router_rate_limit_per_workflow_per_minute' | 'cache_max_tarball_bytes' | 'cache_ttl_days' | 'lockfile_cache_max' | 'lockfile_cache_max_bytes' | 'lockfile_cache_ttl_ms' | 'content_cache_max' | 'content_cache_max_bytes' | 'content_cache_ttl_ms' | 'global_eval_round_timeout_ms' | 'global_eval_candidate_timeout_ms' | 'global_eval_cache_max' | 'global_eval_wait_timeout_ms' | 'check_run_tracking_ttl_days' | 'run_retention_days' | 'audit_retention_days' | 'provenance_retention_days' | 'held_run_retention_days' | 'concurrency_wait_timeout_ms' | 'agent_token_ttl_ms' | 'ownership_db_check_timeout_ms' | 'sealed_secrets_retry_backoff_ms' | 'unroutable_grace_ms' | 'ingest_overflow_claim_timeout_ms' | 'scaler_reap_interval_ms' | 'scaler_reap_stranded_timeout_ms' | 'scaler_reap_reattempt_interval_ms' | 'scaler_claim_retention_ms' | 'scaler_provision_backoff_base_ms' | 'scaler_provision_backoff_max_ms' | 'scaler_provision_max_consecutive_failures';
4
+ export type ClusterNumberColumn = 'max_github_payload_bytes' | 'event_log_max_payload_bytes' | 'lock_file_max_bytes' | 'webhook_dedup_ttl_ms' | 'event_router_event_ttl_seconds' | 'event_router_max_dispatch_attempts' | 'queue_max_depth' | 'reroute_flap_grace_ms' | 'max_fanout_hosts' | 'event_router_rate_limit_per_workflow_per_minute' | 'cache_max_tarball_bytes' | 'cache_ttl_days' | 'lockfile_cache_max' | 'lockfile_cache_max_bytes' | 'lockfile_cache_ttl_ms' | 'content_cache_max' | 'content_cache_max_bytes' | 'content_cache_ttl_ms' | 'global_eval_round_timeout_ms' | 'global_eval_candidate_timeout_ms' | 'global_eval_cache_max' | 'global_eval_wait_timeout_ms' | 'firecracker_api_socket_wait_ms' | 'check_run_tracking_ttl_days' | 'run_retention_days' | 'audit_retention_days' | 'provenance_retention_days' | 'held_run_retention_days' | 'concurrency_wait_timeout_ms' | 'agent_token_ttl_ms' | 'ownership_db_check_timeout_ms' | 'sealed_secrets_retry_backoff_ms' | 'unroutable_grace_ms' | 'ingest_overflow_claim_timeout_ms' | 'scaler_reap_interval_ms' | 'scaler_reap_stranded_timeout_ms' | 'scaler_reap_reattempt_interval_ms' | 'scaler_claim_retention_ms' | 'scaler_provision_backoff_base_ms' | 'scaler_provision_backoff_max_ms' | 'scaler_provision_max_consecutive_failures';
5
5
  /** Text columns on cluster_settings readable via {@link ClusterSettingsReader}. */
6
6
  export type ClusterStringColumn = 'dashboard_verified_issuer';
7
7
  /** Boolean columns on cluster_settings readable via {@link ClusterSettingsReader}. */
@@ -88,6 +88,12 @@ export declare class ClusterSettingsReader {
88
88
  */
89
89
  private loadSnapshot;
90
90
  private loadRow;
91
+ /**
92
+ * The whole row from one read (undefined when there is no row or the read
93
+ * failed). For a caller that needs several columns that agree with each
94
+ * other, such as a value and the row version it belongs to.
95
+ */
96
+ getRow(): Promise<ClusterSettings | undefined>;
91
97
  /** Resolve a fleet-wide numeric knob; `fallback` is the config.ts cluster default. */
92
98
  getNumber(column: ClusterNumberColumn, fallback: number): Promise<number>;
93
99
  /**
@@ -0,0 +1,45 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { type Logger } from '@kici-dev/shared';
3
+ import type { Database } from '../db/types.js';
4
+ import { PeerCredentialStore } from './peer-credentials.js';
5
+ /** What one reconciliation did. */
6
+ export declare enum CoordinatorCredentialOutcome {
7
+ Valid = "valid",
8
+ Issued = "issued",
9
+ Replaced = "replaced",
10
+ Revoked = "revoked",
11
+ Failed = "failed"
12
+ }
13
+ /** Why a new credential was issued, recorded on the issue log line. */
14
+ export declare enum CredentialIssueReason {
15
+ NoCredential = "no-credential",
16
+ Expired = "expired",
17
+ FileMissing = "credential-file-missing",
18
+ FileMismatch = "credential-file-mismatch"
19
+ }
20
+ export declare const COORDINATOR_CREDENTIAL_REVOKED_MESSAGE = "Peer credential for this coordinator was revoked; not issuing a new one";
21
+ export type CoordinatorCredentialStore = Pick<PeerCredentialStore, 'save' | 'findUnrevokedByInstanceId' | 'findLatestRevokedByInstanceId' | 'retireSelfIssued'>;
22
+ export interface EnsureCoordinatorCredentialOptions {
23
+ store: CoordinatorCredentialStore;
24
+ /** Absolute path of the credential file, with `~` already expanded. */
25
+ credentialFile: string;
26
+ instanceId: string;
27
+ /** Whether an instance has a live `cluster_instances` heartbeat. */
28
+ isInstanceLive: (instanceId: string) => Promise<boolean>;
29
+ logger?: Logger;
30
+ now?: () => Date;
31
+ }
32
+ /**
33
+ * Make sure this coordinator holds a credential its peers accept, issuing one
34
+ * when it has none. Never throws: a failure is logged and reported as `Failed`.
35
+ */
36
+ export declare function ensureCoordinatorCredential(opts: EnsureCoordinatorCredentialOptions): Promise<CoordinatorCredentialOutcome>;
37
+ /** The `selfIssue` callback the coordinator's `PeerAuthCoordinator` runs. */
38
+ export declare function coordinatorSelfIssuer(opts: {
39
+ db: Kysely<Database>;
40
+ credentialFile: string;
41
+ instanceId: string;
42
+ agentMaxReconnectDelayMs: number;
43
+ clusterInstanceHeartbeatMs: number;
44
+ }): () => Promise<CoordinatorCredentialOutcome>;
45
+ //# sourceMappingURL=coordinator-credential.d.ts.map
@@ -10,7 +10,7 @@
10
10
  * - "Peers report step-by-step progress back to coordinator"
11
11
  * - "Cancel mode: graceful -- finish current step, cancel remaining"
12
12
  */
13
- import type { JobReroute, JobProgress, JobProgressAck, PeerScalerEvent, PeerToPeerMessage, ResourceRequest, LabelMatcher } from '@kici-dev/engine';
13
+ import type { JobReroute, JobProgress, JobProgressAck, PeerScalerEvent, PeerToPeerMessage, RerouteSpawnRetry, ResourceRequest, LabelMatcher } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
15
  import type { PeerClient } from './peer-client.js';
16
16
  import type { Dispatcher } from '../agent/dispatcher.js';
@@ -114,6 +114,14 @@ export interface RunCoordinatorDeps {
114
114
  * {@link DEFAULT_MAX_HOPS}.
115
115
  */
116
116
  getRerouteMaxHops?: (job: JobToRoute) => Promise<number>;
117
+ /**
118
+ * Per-job spawn-retry budget sent to the receiving peer on `job.reroute` and
119
+ * enforced here on the peer's relayed spawn failures. Resolves the job's org
120
+ * overrides from `org_settings.reroute_spawn_max_attempts` /
121
+ * `reroute_spawn_retry_backoff_ms`, else the cluster defaults. When absent,
122
+ * {@link DEFAULT_REROUTE_SPAWN_RETRY} is used.
123
+ */
124
+ getRerouteSpawnRetry?: (job: JobToRoute) => Promise<RerouteSpawnRetry>;
117
125
  }
118
126
  export declare class RunCoordinator {
119
127
  private readonly instanceId;
@@ -128,6 +136,7 @@ export declare class RunCoordinator {
128
136
  private readonly getRerouteSpawnWindowMs;
129
137
  private readonly getRerouteAckTimeoutMs;
130
138
  private readonly getRerouteMaxHops;
139
+ private readonly getRerouteSpawnRetry;
131
140
  /**
132
141
  * Tracks which jobs have been rerouted to which peers, keyed by runId.
133
142
  * Used for cancel propagation, progress tracking, and the post-ACK spawn
@@ -193,12 +202,18 @@ export declare class RunCoordinator {
193
202
  * line + dispatch-queue last-error column) just as it would for a
194
203
  * locally-emitted scaler event.
195
204
  *
196
- * Layer B fast path: a `scaler.failed` for a tracked rerouted job whose spawn
197
- * window is still armed is the worker's NAK-after-accept — the peer accepted
198
- * the reroute but its agent spawn failed asynchronously before reporting any
199
- * progress. Re-dispatch immediately (the same routine the spawn-window timer
200
- * runs) instead of waiting the window out. Idempotent with the Layer A timer:
201
- * whichever fires first removes the tracking entry.
205
+ * Layer B: a `scaler.failed` for a tracked rerouted job whose spawn window is
206
+ * still armed is the worker reporting that an agent spawn failed after it
207
+ * accepted the reroute. The worker retries within the spawn-retry budget sent on
208
+ * `job.reroute`, and the relay carries its verdict:
209
+ *
210
+ * - `final: true` — the worker gave the job back. Re-dispatch immediately (the
211
+ * same routine the spawn-window timer runs). Idempotent with the Layer A
212
+ * timer: whichever fires first removes the tracking entry.
213
+ * - `final: false` — the worker is retrying. Re-arm the window for one more
214
+ * attempt plus the backoff; treat the `maxAttempts`-th such relay as final.
215
+ * - no verdict — an older worker that retries without a bound. The window
216
+ * armed at the reroute keeps running unchanged.
202
217
  *
203
218
  * Two guards make the re-dispatch safe. First, source provenance: the failure
204
219
  * must come from `fromPeerId === tracked.peerId` (the authenticated connection
@@ -211,6 +226,16 @@ export declare class RunCoordinator {
211
226
  * correct regardless of which peer relayed the event.
212
227
  */
213
228
  onPeerScalerEvent(msg: PeerScalerEvent, fromPeerId: string): void;
229
+ /**
230
+ * A worker's verdict on a failed spawn for a job it holds. `final: true` means it
231
+ * gave the job back, so re-dispatch now. `final: false` means it is retrying, so
232
+ * restart the window for one more attempt plus the backoff this coordinator told it
233
+ * to wait. No verdict means a worker that retries without a bound, so leave the
234
+ * window as it is. The count caps a worker that never says `final`.
235
+ */
236
+ private onRelayedSpawnFailure;
237
+ /** Restart a rerouted job's spawn window for one more worker attempt plus its backoff. */
238
+ private rearmAfterSpawnRetry;
214
239
  /**
215
240
  * Handle peer job completion. Updates ExecutionTracker.
216
241
  * The execution tracker's onJobStatus fires the needs-aware scheduler hook,
@@ -317,7 +342,8 @@ export declare class RunCoordinator {
317
342
  /**
318
343
  * Post-ACK spawn-window backstop. Fires when a rerouted job's peer accepted
319
344
  * but produced no progress within the window (Layer A), or immediately when a
320
- * worker relays a `scaler.failed` for the job (Layer B, via onPeerScalerEvent).
345
+ * worker relays its final spawn-failure verdict for the job (Layer B, via
346
+ * onPeerScalerEvent).
321
347
  * Best-effort cancels the original peer (double-execution guard against a
322
348
  * slow-but-healthy spawn), then re-runs routing to another peer, then a local
323
349
  * fallback, and finally records the job failed rather than leaving it pending.
@@ -334,9 +360,25 @@ export declare class RunCoordinator {
334
360
  * The run-cancel path needs this for a job this coordinator never routed:
335
361
  * the Platform picks any pool member to receive `run.cancel.request`, and only
336
362
  * the coordinator holding the agent's socket can put a cancel frame on it.
337
- * {@link RunCoordinator.cancelRun} covers rerouted jobs and is unchanged.
363
+ * `force` rides on the message, so a force cancel stays forced on the peer.
364
+ * {@link RunCoordinator.cancelRun} covers rerouted jobs.
365
+ */
366
+ cancelJobOnPeer(peerId: string, runId: string, jobId: string, reason: string, force?: boolean): boolean;
367
+ /**
368
+ * Send a fire-and-forget message to a peer. The outbound client carries it
369
+ * when connected; otherwise the peer's own connection to this process does.
370
+ * The outbound client is down while it reconnects, and for good on a
371
+ * coordinator whose credential an operator revoked.
372
+ * `PeerClient.send` returns false only when it sent nothing, so the fallback
373
+ * never delivers a message twice.
374
+ */
375
+ private sendToPeer;
376
+ /**
377
+ * The link a reroute uses, chosen before sending. A reroute is never retried
378
+ * over the second link: `false` from a connected client is a NAK or an ack
379
+ * timeout, and resending would deliver the job twice.
338
380
  */
339
- cancelJobOnPeer(peerId: string, runId: string, jobId: string, reason: string): boolean;
381
+ private rerouteTransport;
340
382
  /**
341
383
  * Local fallback for a re-dispatch: dispatch the job on this coordinator under
342
384
  * the existing jobId. Returns true when the local dispatcher accepts it.
@@ -9,6 +9,7 @@ export type { PeerInfo, PeerAgentInfo, PeerRegistryOptions } from './peer-regist
9
9
  export { PeerClient } from './peer-client.js';
10
10
  export type { PeerClientOptions } from './peer-client.js';
11
11
  export { PeerAuthCoordinator } from './peer-auth-coordinator.js';
12
+ export { coordinatorSelfIssuer } from './coordinator-credential.js';
12
13
  export type { AuthDecision, RejectionAction } from './peer-auth-coordinator.js';
13
14
  export { createPeerHandler } from './peer-handler.js';
14
15
  export type { PeerHandlerDeps } from './peer-handler.js';
@@ -1,4 +1,4 @@
1
- import type { Kysely } from 'kysely';
1
+ import { type Kysely } from 'kysely';
2
2
  import type { Database } from '../db/types.js';
3
3
  /** Default interval between `cluster_instances` heartbeat writes. */
4
4
  export declare const DEFAULT_INSTANCE_HEARTBEAT_MS = 10000;
@@ -23,6 +23,11 @@ export declare const DEFAULT_RECOVERY_GRACE_MS = 120000;
23
23
  * recovery machinery would have given it to reconnect.
24
24
  */
25
25
  export declare function instanceLivenessGraceMs(gracePeriodMs: number, heartbeatMs?: number): number;
26
+ /**
27
+ * Whether `instanceId` has a `cluster_instances` heartbeat inside `graceMs`:
28
+ * the same liveness the dispatch plane's ownership predicate reads.
29
+ */
30
+ export declare function isInstanceLive(db: Kysely<Database>, instanceId: string, graceMs: number): Promise<boolean>;
26
31
  export interface InstanceHeartbeatOptions {
27
32
  db: Kysely<Database>;
28
33
  instanceId: string;
@@ -18,6 +18,8 @@ import { Kysely } from 'kysely';
18
18
  * and a stray log line would break the contract).
19
19
  */
20
20
  export declare function silenceJoinTokenLogger(): void;
21
+ export declare const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Create a new token with: kici-admin peer create-token";
22
+ export declare const TOKEN_EXPIRED_MESSAGE = "Join token has expired. Create a new token with: kici-admin peer create-token";
21
23
  interface TokenRouting {
22
24
  orgId: string;
23
25
  routingKey: string;
@@ -1,4 +1,5 @@
1
1
  import { type CredentialFileData } from './peer-credentials.js';
2
+ import { CoordinatorCredentialOutcome } from './coordinator-credential.js';
2
3
  export type AuthDecision = {
3
4
  mode: 'credential';
4
5
  credential: CredentialFileData;
@@ -15,6 +16,11 @@ export declare class PeerAuthCoordinator {
15
16
  private readonly instanceId;
16
17
  private readonly joinToken?;
17
18
  private readonly joinWaitTimeoutMs;
19
+ private readonly selfIssue?;
20
+ /** The issuance in progress; concurrent sibling clients share it. */
21
+ private selfIssueInFlight;
22
+ /** An operator revoked this coordinator's credential: never issue again in this process. */
23
+ private selfIssueRevoked;
18
24
  /** Promise-chain mutex tail; every file op awaits the prior one. */
19
25
  private lock;
20
26
  /** Set while one peer-client is mid token-join; siblings await it. */
@@ -24,11 +30,18 @@ export declare class PeerAuthCoordinator {
24
30
  instanceId: string;
25
31
  joinToken?: string;
26
32
  joinWaitTimeoutMs?: number;
33
+ /**
34
+ * Issues this coordinator its own credential when a decision finds no
35
+ * credential and no join token. Only a coordinator wires it.
36
+ */
37
+ selfIssue?: () => Promise<CoordinatorCredentialOutcome>;
27
38
  });
28
39
  /** Run `fn` exclusively against the credential file. */
29
40
  private withLock;
30
41
  private readValidCredential;
31
42
  decideAuth(): Promise<AuthDecision>;
43
+ /** Run the issuer under the mutex, once for every concurrent caller. */
44
+ private runSelfIssue;
32
45
  private makeComplete;
33
46
  private awaitInFlightJoin;
34
47
  reportRejection(provedCredential: string | null, reason: string): Promise<RejectionAction>;
@@ -10,9 +10,13 @@
10
10
  * Authentication uses ECDH key exchange followed by join token (first connect)
11
11
  * or HMAC credential proof (reconnection).
12
12
  */
13
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerClusterSettingsRequest, type PeerClusterSettingsResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
13
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerScalerOrphansRequest, type PeerForgetRequest, type PeerForgetResponse, type PeerScalerOrphansResponse, type PeerClusterSettingsRequest, type PeerClusterSettingsResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
+ import { type PEER_FORGET_TIMEOUT, type PeerForgetRequestHandler } from './peer-forget.js';
16
+ import { type SCALER_ORPHANS_TIMEOUT, type ScalerOrphansRequestHandler } from './scaler-orphans-peer.js';
15
17
  import type { PeerAuthCoordinator } from './peer-auth-coordinator.js';
18
+ /** Logged when this instance has neither a credential file nor a join token. */
19
+ export declare const NO_AUTH_METHOD_MESSAGE = "No peer auth method: this instance has no credential file and no join token";
16
20
  type PeerConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'authenticating' | 'connected';
17
21
  export interface PeerClientOptions {
18
22
  /** WebSocket URL of the remote peer orchestrator. */
@@ -95,6 +99,17 @@ export interface PeerClientOptions {
95
99
  * If undefined, incoming collect requests are ignored.
96
100
  */
97
101
  onLogsCollectRequest?: PeerLogsCollectResponder;
102
+ /**
103
+ * Answers a peer.scaler.orphans.request the connected coordinator forwarded
104
+ * to this node (`kici-admin scaler orphans --target`). If undefined,
105
+ * requests are answered with ok=false.
106
+ */
107
+ onScalerOrphansRequest?: ScalerOrphansRequestHandler;
108
+ /**
109
+ * Forgets a departed peer the connected coordinator forgot (`kici-admin peer
110
+ * forget`). If undefined, requests are answered with outcome `error`.
111
+ */
112
+ onPeerForgetRequest?: PeerForgetRequestHandler;
98
113
  }
99
114
  /**
100
115
  * Builds a node's subtree bundle in response to a peer.logs.collect.request and
@@ -108,12 +123,16 @@ export declare class PeerClient {
108
123
  private heartbeatTimer;
109
124
  private reconnectTimer;
110
125
  private reconnectAttempts;
126
+ /** Whether this client already warned that it has no auth method; re-armed by an accepted auth. */
127
+ private noAuthWarned;
111
128
  private intentionalDisconnect;
112
129
  private _targetInstanceId;
113
130
  private sessionKey;
114
131
  private readonly ackWaiters;
115
132
  private readonly cacheWaiters;
116
133
  private readonly configReloadWaiters;
134
+ private readonly scalerOrphansWaiters;
135
+ private readonly peerForgetWaiters;
117
136
  private readonly clusterSettingsWaiters;
118
137
  /** Correlates peer.logs.collect.request with the peer's chunked subtree response. */
119
138
  private readonly logsCollectWaiters;
@@ -146,6 +165,8 @@ export declare class PeerClient {
146
165
  private readonly onPeerConfigReload?;
147
166
  private readonly onAuthenticated?;
148
167
  private readonly onLogsCollectRequest?;
168
+ private readonly onScalerOrphansRequest?;
169
+ private readonly onPeerForgetRequest?;
149
170
  constructor(options: PeerClientOptions);
150
171
  /** Current connection state. */
151
172
  get state(): PeerConnectionState;
@@ -185,10 +206,26 @@ export declare class PeerClient {
185
206
  */
186
207
  sendConfigReloadAndWait(msg: PeerConfigReload, timeoutMs?: number): Promise<PeerConfigReloadResponse | null>;
187
208
  /**
188
- * Send a peer.clusterSettings.request to the connected leader and wait for the
189
- * matching peer.clusterSettings.response carrying the worker-settings snapshot.
209
+ * Send a peer.forget.request to the connected peer and wait for the matching
210
+ * response.
211
+ *
212
+ * @returns the response; null when not connected; `'timeout'` when no
213
+ * response arrived within `timeoutMs`.
214
+ */
215
+ sendPeerForgetAndWait(msg: PeerForgetRequest, timeoutMs: number): Promise<PeerForgetResponse | null | typeof PEER_FORGET_TIMEOUT>;
216
+ /**
217
+ * Send a peer.scaler.orphans.request to the connected peer and wait for the
218
+ * matching response.
219
+ *
220
+ * @returns the response; null when not connected; `'timeout'` when no
221
+ * response arrived within `timeoutMs`.
222
+ */
223
+ sendScalerOrphansAndWait(msg: PeerScalerOrphansRequest, timeoutMs: number): Promise<PeerScalerOrphansResponse | null | typeof SCALER_ORPHANS_TIMEOUT>;
224
+ /**
225
+ * Send a peer.clusterSettings.request to the connected coordinator and wait for
226
+ * the matching peer.clusterSettings.response carrying the worker-settings snapshot.
190
227
  *
191
- * @returns The response, or null if not connected or the leader doesn't reply
228
+ * @returns The response, or null if not connected or the coordinator doesn't reply
192
229
  * within the timeout — the caller keeps its current (config-default) settings.
193
230
  */
194
231
  sendClusterSettingsRequestAndWait(msg: PeerClusterSettingsRequest, timeoutMs?: number): Promise<PeerClusterSettingsResponse | null>;
@@ -1,4 +1,15 @@
1
1
  import { Kysely } from 'kysely';
2
+ /** How a credential row was issued, recorded in `peer_credentials.metadata.issuance`. */
3
+ export declare enum PeerCredentialIssuance {
4
+ /** A coordinator issued the credential to itself (`coordinator-credential.ts`). */
5
+ Self = "self"
6
+ }
7
+ /**
8
+ * `metadata` key set when a coordinator retires the self-issued credential its
9
+ * previous run left behind. An operator revoke never sets it, which is how a
10
+ * retirement is told apart from a revoke.
11
+ */
12
+ export declare const RETIRED_BY_INSTANCE_KEY = "retiredByInstance";
2
13
  /**
3
14
  * A peer credential record stored in the coordinator's database.
4
15
  */
@@ -14,6 +25,7 @@ export interface PeerCredential {
14
25
  lastValidatedBy: string | null;
15
26
  expiresAt: Date;
16
27
  revokedAt: Date | null;
28
+ metadata: Record<string, unknown>;
17
29
  }
18
30
  /**
19
31
  * Data persisted in this orchestrator's local credential file
@@ -55,6 +67,7 @@ export declare class PeerCredentialStore {
55
67
  routingKeys: string[];
56
68
  sourceTokenHash?: string;
57
69
  expiryDays?: number;
70
+ metadata?: Record<string, unknown>;
58
71
  }): Promise<{
59
72
  revokedCount: number;
60
73
  }>;
@@ -66,6 +79,24 @@ export declare class PeerCredentialStore {
66
79
  * Find a non-revoked, non-expired credential by instance ID.
67
80
  */
68
81
  findByInstanceId(instanceId: string): Promise<PeerCredential | null>;
82
+ /**
83
+ * The credential row for `instanceId` that nobody revoked, expired or not.
84
+ * `peer_credentials_active_uniq` allows at most one.
85
+ */
86
+ findUnrevokedByInstanceId(instanceId: string): Promise<PeerCredential | null>;
87
+ /** The most recently revoked credential row for `instanceId`, or null. */
88
+ findLatestRevokedByInstanceId(instanceId: string): Promise<PeerCredential | null>;
89
+ /**
90
+ * Revoke `instanceId`'s unrevoked, self-issued row whose hash is
91
+ * `credentialHash`, recording who retired it. Every condition sits in the
92
+ * WHERE clause, so a row that changed meanwhile is left alone.
93
+ * Returns whether a row was retired.
94
+ */
95
+ retireSelfIssued(opts: {
96
+ instanceId: string;
97
+ credentialHash: string;
98
+ retiredByInstance: string;
99
+ }): Promise<boolean>;
69
100
  /**
70
101
  * Update the last_seen_at timestamp for a credential.
71
102
  * Optionally records which coordinator validated it.
@@ -0,0 +1,138 @@
1
+ import { PeerForgetOutcome, type PeerForgetRequest, type PeerForgetResponse } from '@kici-dev/engine';
2
+ import { z } from 'zod';
3
+ import type { PeerRegistry } from './peer-registry.js';
4
+ /** What forgetting did on one coordinator. */
5
+ export interface PeerForgetResult {
6
+ /** The coordinator that answered. */
7
+ coordinator: string;
8
+ outcome: PeerForgetOutcome;
9
+ detail: string;
10
+ }
11
+ /** Local outcomes that stop a forget before it reaches any sibling. */
12
+ export declare const LOCAL_REFUSALS: ReadonlySet<PeerForgetOutcome>;
13
+ /** A sibling's answer that never arrived, or a sibling no connection reached. */
14
+ export declare const PEER_FORGET_TIMEOUT = "timeout";
15
+ /** The answer to a forget request a peer that is not a coordinator sent. */
16
+ export declare const PEER_FORGET_COORDINATORS_ONLY = "peer forget requests are accepted from coordinators only";
17
+ /** The answer of a peer with no forget handler wired. */
18
+ export declare const PEER_FORGET_UNHANDLED = "peer forget requests are not handled by this peer";
19
+ /** Which window a coordinator keeps treating a silent peer as alive for. */
20
+ export declare const PeerLivenessWindowKind: z.ZodEnum<{
21
+ "adopter-grace": "adopter-grace";
22
+ "reroute-grace": "reroute-grace";
23
+ "stale-window": "stale-window";
24
+ }>;
25
+ export type PeerLivenessWindowKind = z.infer<typeof PeerLivenessWindowKind>;
26
+ /** How long after its last heartbeat a coordinator still treats a peer as alive. */
27
+ export interface PeerLivenessWindow {
28
+ kind: PeerLivenessWindowKind;
29
+ windowMs: number;
30
+ }
31
+ /** What a coordinator reads to choose a peer's liveness window. */
32
+ export interface PeerLivenessSource {
33
+ /** The peer stale window (`KICI_CLUSTER_PEER_STALE_TIMEOUT_MS`). */
34
+ staleTimeoutMs: number;
35
+ /** Reads the live `reroute_flap_grace_ms` setting. */
36
+ rerouteFlapGraceMs: () => Promise<number>;
37
+ /**
38
+ * This coordinator's event-provision backstop, or null when it runs none.
39
+ * `holdsAdoptedProvisions` reads whether a peer adopted provisions the
40
+ * backstop judges by that peer's liveness.
41
+ */
42
+ backstop: {
43
+ holdsAdoptedProvisions: (instanceId: string) => Promise<boolean>;
44
+ } | null;
45
+ }
46
+ /**
47
+ * The longest window in which this coordinator still treats `instanceId` as
48
+ * alive after its last heartbeat. Forgetting the peer inside it would cut that
49
+ * grace short: every reader of the registry treats a peer it no longer knows
50
+ * as gone at once.
51
+ *
52
+ * - A peer that adopted provisions gets the event-provision backstop's own
53
+ * grace (`reaperFlapGraceMs`), or the backstop tears its instances down.
54
+ * - Any other peer gets the longer of the stale window and the reroute flap
55
+ * grace, which the rerouted-job guard and the master-key requeue give every
56
+ * disconnected peer whether or not this coordinator runs a backstop.
57
+ */
58
+ export declare function peerLivenessWindow(instanceId: string, source: PeerLivenessSource): Promise<PeerLivenessWindow>;
59
+ /** The checks a coordinator runs before it forgets a peer. */
60
+ export interface PeerForgetGuards {
61
+ /**
62
+ * How long after its last heartbeat the peer is still treated as alive. A
63
+ * peer heard from inside it may be partitioned rather than gone, so it is kept.
64
+ */
65
+ liveness: PeerLivenessWindow;
66
+ /**
67
+ * This coordinator's event-provision backstop, or null when it runs none.
68
+ * `configuredPeerCount` is the `KICI_CLUSTER_PEERS` count the backstop's
69
+ * cluster-view check reads beside the live registry.
70
+ */
71
+ backstop: {
72
+ configuredPeerCount: number;
73
+ } | null;
74
+ /** The operator acknowledged that the forget may switch the backstop back on. */
75
+ acknowledgeBackstop: boolean;
76
+ /** The current time; tests pin it. */
77
+ nowMs?: number;
78
+ }
79
+ /**
80
+ * The guards for forgetting `instanceId` on one coordinator, built from its
81
+ * cluster config, the live reroute flap grace and its event-provision
82
+ * backstop. The backstop runs on every coordinator that runs a scaler manager.
83
+ */
84
+ export declare function peerForgetGuards(instanceId: string, cluster: {
85
+ peerStaleTimeoutMs: number;
86
+ peers: readonly unknown[];
87
+ }, liveness: Omit<PeerLivenessSource, 'staleTimeoutMs'>, acknowledgeBackstop: boolean): Promise<PeerForgetGuards>;
88
+ /** What forgetting a peer that would switch the backstop back on does. */
89
+ export declare function backstopConsequence(instanceId: string): string;
90
+ /**
91
+ * Drop `instanceId` from `registry` when it names a peer that left the
92
+ * cluster. Kept, with the reason: a connected peer (its next heartbeat would
93
+ * register it again), a peer heard from inside its liveness window (it may be
94
+ * partitioned rather than gone; see `peerLivenessWindow`), and a peer whose removal would switch the
95
+ * event-provision backstop back on, unless the request acknowledged that.
96
+ */
97
+ export declare function forgetDepartedPeer(registry: PeerRegistry, selfInstanceId: string, instanceId: string, guards: PeerForgetGuards): Omit<PeerForgetResult, 'coordinator'>;
98
+ /** Answers a forget request a sibling coordinator fanned out. */
99
+ export type PeerForgetRequestHandler = (msg: PeerForgetRequest) => Promise<{
100
+ outcome: PeerForgetOutcome;
101
+ detail: string;
102
+ }>;
103
+ /**
104
+ * Answer one forget request with `handler`. A coordinator with no handler
105
+ * answers {@link PEER_FORGET_UNHANDLED}, and a handler that fails (its guards
106
+ * read the database) answers `error`, so the peer is kept and the operator
107
+ * sees why.
108
+ */
109
+ export declare function replyToPeerForgetRequest(msg: PeerForgetRequest, handler: PeerForgetRequestHandler | undefined, send: (response: PeerForgetResponse) => void, logFields?: Record<string, unknown>): void;
110
+ /**
111
+ * Sends one forget request to a sibling coordinator and waits for its answer.
112
+ * Resolves null when no connection reached the sibling, and
113
+ * {@link PEER_FORGET_TIMEOUT} when it did not answer within `timeoutMs`.
114
+ */
115
+ export type SendPeerForget = (siblingInstanceId: string, msg: PeerForgetRequest, timeoutMs: number) => Promise<PeerForgetResponse | null | typeof PEER_FORGET_TIMEOUT>;
116
+ /**
117
+ * Forget `instanceId` on this coordinator, then on every connected sibling
118
+ * coordinator, each of which holds its own registry. A peer this coordinator
119
+ * still has connected is refused and not fanned out. The first result is this
120
+ * coordinator's.
121
+ */
122
+ export declare function forgetAcrossCoordinators(registry: PeerRegistry, selfInstanceId: string, instanceId: string, timeoutMs: number, guards: PeerForgetGuards, send: SendPeerForget): Promise<PeerForgetResult[]>;
123
+ /**
124
+ * The coordinator peers the cluster expects but this coordinator has no live
125
+ * link to: registered coordinators whose connection is down.
126
+ */
127
+ export declare function disconnectedCoordinatorIds(registry: PeerRegistry, selfInstanceId: string): string[];
128
+ /** Pending forget requests sent to siblings, keyed by message id. */
129
+ export declare class PeerForgetWaiters {
130
+ private readonly waiters;
131
+ /** Wait for the response to `messageId`, or {@link PEER_FORGET_TIMEOUT}. */
132
+ wait(messageId: string, timeoutMs: number): Promise<PeerForgetResponse | typeof PEER_FORGET_TIMEOUT>;
133
+ /** Hand a response to the request waiting for it. An unknown id is dropped. */
134
+ resolve(response: PeerForgetResponse): void;
135
+ /** End every pending wait with an `error` response naming `reason`. */
136
+ rejectAll(reason: string): void;
137
+ }
138
+ //# sourceMappingURL=peer-forget.d.ts.map
@@ -6,8 +6,10 @@
6
6
  * proof (reconnection), registers the peer in PeerRegistry, and routes
7
7
  * messages bidirectionally. Sends periodic heartbeats to the connecting peer.
8
8
  */
9
- import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerClusterSettingsRequest, type WorkerClusterSettings, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
9
+ import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerScalerOrphansRequest, type PeerForgetRequest, type PeerForgetResponse, type PeerScalerOrphansResponse, type PeerClusterSettingsRequest, type WorkerClusterSettings, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
10
10
  import type { PeerRegistry } from './peer-registry.js';
11
+ import { type PEER_FORGET_TIMEOUT, type PeerForgetRequestHandler } from './peer-forget.js';
12
+ import { type SCALER_ORPHANS_TIMEOUT, type ScalerOrphansRequestHandler } from './scaler-orphans-peer.js';
11
13
  import type { PeerCredentialStore } from './peer-credentials.js';
12
14
  import { type JoinTokenManager } from './join-token.js';
13
15
  /**
@@ -104,6 +106,19 @@ export interface PeerHandlerDeps {
104
106
  * If undefined, incoming collect requests are ignored.
105
107
  */
106
108
  onLogsCollectRequest?: (msg: PeerLogsCollectRequest, send: (out: PeerToPeerMessage) => boolean) => Promise<void>;
109
+ /**
110
+ * Answers a peer.scaler.orphans.request a coordinator forwarded to this node
111
+ * (`kici-admin scaler orphans --target`). A request from a peer that is not
112
+ * a coordinator is refused before it reaches the handler. If undefined,
113
+ * requests are answered with ok=false.
114
+ */
115
+ onScalerOrphansRequest?: ScalerOrphansRequestHandler;
116
+ /**
117
+ * Forgets a departed peer a sibling coordinator forgot (`kici-admin peer
118
+ * forget`). A request from a peer that is not a coordinator is refused. If
119
+ * undefined, requests are answered with outcome `error`.
120
+ */
121
+ onPeerForgetRequest?: PeerForgetRequestHandler;
107
122
  }
108
123
  /**
109
124
  * Create a handler function for incoming peer WebSocket connections.
@@ -127,6 +142,8 @@ export declare function createPeerHandler(deps: PeerHandlerDeps): {
127
142
  closePeer: (targetInstanceId: string, code: number, reason: string) => void;
128
143
  sendAndWaitAck: (targetInstanceId: string, msg: JobReroute, timeoutMs?: number) => Promise<boolean>;
129
144
  sendConfigReloadAndWait: (targetInstanceId: string, msg: PeerConfigReload, timeoutMs?: number) => Promise<PeerConfigReloadResponse | null>;
145
+ sendScalerOrphansAndWait: (targetInstanceId: string, msg: PeerScalerOrphansRequest, timeoutMs: number) => Promise<PeerScalerOrphansResponse | null | typeof SCALER_ORPHANS_TIMEOUT>;
146
+ sendPeerForgetAndWait: (targetInstanceId: string, msg: PeerForgetRequest, timeoutMs: number) => Promise<PeerForgetResponse | null | typeof PEER_FORGET_TIMEOUT>;
130
147
  sendLogsCollectAndWait: (targetInstanceId: string, msg: PeerLogsCollectRequest, timeoutMs: number) => Promise<Buffer>;
131
148
  getConnectionCount: () => number;
132
149
  broadcastHeartbeat: (inventory: Omit<PeerHeartbeat, 'type'>) => void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The peer-channel half of `kici-admin scaler orphans --target`: answering a
3
+ * forwarded peer.scaler.orphans.request, and correlating the response with
4
+ * the request that is waiting for it. Shared by the outgoing `PeerClient` and
5
+ * the incoming peer handler, which carry the same message pair.
6
+ */
7
+ import { z } from 'zod';
8
+ import type { PeerScalerOrphansRequest, PeerScalerOrphansResponse } from '@kici-dev/engine';
9
+ import type { ScalerOrphansAnswer } from '../scaler/orphan-requests.js';
10
+ /** Answers a forwarded request from this node's own host and tracking. */
11
+ export type ScalerOrphansRequestHandler = (msg: PeerScalerOrphansRequest) => Promise<ScalerOrphansAnswer>;
12
+ /** The answer of a peer that has no handler wired. */
13
+ export declare const SCALER_ORPHANS_UNHANDLED_ERROR = "scaler orphan requests are not handled by this peer";
14
+ /** The answer to a request a worker sent: a worker never stops another node's VMs. */
15
+ export declare const SCALER_ORPHANS_COORDINATORS_ONLY_ERROR = "scaler orphan requests are accepted from coordinators only";
16
+ /** A forward that reached no peer, or a peer that did not answer in time. */
17
+ export declare const ScalerOrphansForwardFailure: z.ZodEnum<{
18
+ "not-connected": "not-connected";
19
+ timeout: "timeout";
20
+ }>;
21
+ export type ScalerOrphansForwardFailure = z.infer<typeof ScalerOrphansForwardFailure>;
22
+ /** A wait that ended without a response. */
23
+ export declare const SCALER_ORPHANS_TIMEOUT: "timeout";
24
+ /**
25
+ * Answer one forwarded request through `send`. A missing handler and a
26
+ * handler failure are both answered `ok: false`, so the requester never waits
27
+ * out its timeout for a peer that cannot answer.
28
+ */
29
+ export declare function replyToScalerOrphansRequest(msg: PeerScalerOrphansRequest, handler: ScalerOrphansRequestHandler | undefined, send: (response: PeerScalerOrphansResponse) => void, logFields?: Record<string, unknown>): void;
30
+ /** Pending scaler orphan requests, keyed by message id. */
31
+ export declare class ScalerOrphansWaiters {
32
+ private readonly waiters;
33
+ /** Wait for the response to `messageId`, or {@link SCALER_ORPHANS_TIMEOUT}. */
34
+ wait(messageId: string, timeoutMs: number): Promise<PeerScalerOrphansResponse | typeof SCALER_ORPHANS_TIMEOUT>;
35
+ /** Hand a response to the request waiting for it. An unknown id is dropped. */
36
+ resolve(response: PeerScalerOrphansResponse): void;
37
+ /** End every pending wait with an `ok: false` response naming `reason`. */
38
+ rejectAll(reason: string): void;
39
+ }
40
+ //# sourceMappingURL=scaler-orphans-peer.d.ts.map