@kici-dev/orchestrator 0.13.0 → 0.14.1

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 (58) hide show
  1. package/dist/agent/dispatcher.d.ts +17 -0
  2. package/dist/cancel/peer-job-cancel.d.ts +12 -0
  3. package/dist/cli/api-client.d.ts +73 -0
  4. package/dist/cli/commands/cluster-settings.d.ts +31 -1
  5. package/dist/cli/commands/peer.d.ts +27 -1
  6. package/dist/cli/commands/scaler.d.ts +40 -4
  7. package/dist/cli.js +2210 -933
  8. package/dist/cluster/cluster-settings-reader.d.ts +8 -2
  9. package/dist/cluster/coordinator.d.ts +34 -8
  10. package/dist/cluster/peer-client.d.ts +37 -4
  11. package/dist/cluster/peer-forget.d.ts +138 -0
  12. package/dist/cluster/peer-handler.d.ts +18 -1
  13. package/dist/cluster/scaler-orphans-peer.d.ts +40 -0
  14. package/dist/cluster/settings-propagation.d.ts +85 -0
  15. package/dist/cluster/worker-cluster-settings.d.ts +29 -0
  16. package/dist/config.d.ts +9 -0
  17. package/dist/db/migrations/154_cluster_settings_firecracker_api_socket_wait.d.ts +4 -0
  18. package/dist/db/migrations/155_org_settings_reroute_spawn_retry.d.ts +17 -0
  19. package/dist/db/migrations/156_kici_events_match_outcome.d.ts +17 -0
  20. package/dist/db/types.d.ts +16 -0
  21. package/dist/events/event-router.d.ts +7 -1
  22. package/dist/events/event-store.d.ts +20 -3
  23. package/dist/events/types.d.ts +45 -0
  24. package/dist/helpers/windows-batch.d.ts +59 -0
  25. package/dist/index.js +60 -0
  26. package/dist/metrics/prometheus.d.ts +2 -0
  27. package/dist/orchestrator-core.d.ts +7 -0
  28. package/dist/registration/registration-store.d.ts +10 -0
  29. package/dist/routes/admin-cluster-settings.d.ts +13 -1
  30. package/dist/routes/admin-event-inspect.d.ts +36 -0
  31. package/dist/routes/admin-peer-forget.d.ts +43 -0
  32. package/dist/routes/admin-scaler-orphans.d.ts +64 -0
  33. package/dist/routes/admin.d.ts +23 -0
  34. package/dist/scaler/backend-factory.d.ts +6 -1
  35. package/dist/scaler/bare-metal-backend.d.ts +17 -15
  36. package/dist/scaler/bare-metal-launch.d.ts +90 -0
  37. package/dist/scaler/firecracker-backend.d.ts +181 -7
  38. package/dist/scaler/live-vms.d.ts +57 -0
  39. package/dist/scaler/manager.d.ts +112 -3
  40. package/dist/scaler/orphan-requests.d.ts +23 -0
  41. package/dist/scaler/overlay-template.d.ts +38 -0
  42. package/dist/scaler/scaler-state-store.d.ts +7 -0
  43. package/dist/scaler/spawn-errors.d.ts +17 -0
  44. package/dist/scaler/startup-orphan-sweep.d.ts +15 -0
  45. package/dist/scaler/types.d.ts +21 -2
  46. package/dist/scaler/vm-job-bindings.d.ts +17 -0
  47. package/dist/secrets/rbac.d.ts +7 -1
  48. package/dist/server.js +5498 -2193
  49. package/dist/standalone.js +10754 -7451
  50. package/dist/worker/in-memory-execution-tracker.d.ts +5 -0
  51. package/dist/worker/in-memory-job-queue.d.ts +21 -1
  52. package/dist/worker/queued-reroute-release.d.ts +39 -0
  53. package/dist/worker/reroute-spawn-control.d.ts +58 -0
  54. package/dist/worker/spawn-retry-budget.d.ts +87 -0
  55. package/dist/worker-core.d.ts +11 -2
  56. package/installer-image-digests.json +3 -3
  57. package/package.json +4 -4
  58. 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
  /**
@@ -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.
@@ -10,8 +10,10 @@
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';
16
18
  /** Logged when this instance has neither a credential file nor a join token. */
17
19
  export declare const NO_AUTH_METHOD_MESSAGE = "No peer auth method: this instance has no credential file and no join token";
@@ -97,6 +99,17 @@ export interface PeerClientOptions {
97
99
  * If undefined, incoming collect requests are ignored.
98
100
  */
99
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;
100
113
  }
101
114
  /**
102
115
  * Builds a node's subtree bundle in response to a peer.logs.collect.request and
@@ -118,6 +131,8 @@ export declare class PeerClient {
118
131
  private readonly ackWaiters;
119
132
  private readonly cacheWaiters;
120
133
  private readonly configReloadWaiters;
134
+ private readonly scalerOrphansWaiters;
135
+ private readonly peerForgetWaiters;
121
136
  private readonly clusterSettingsWaiters;
122
137
  /** Correlates peer.logs.collect.request with the peer's chunked subtree response. */
123
138
  private readonly logsCollectWaiters;
@@ -150,6 +165,8 @@ export declare class PeerClient {
150
165
  private readonly onPeerConfigReload?;
151
166
  private readonly onAuthenticated?;
152
167
  private readonly onLogsCollectRequest?;
168
+ private readonly onScalerOrphansRequest?;
169
+ private readonly onPeerForgetRequest?;
153
170
  constructor(options: PeerClientOptions);
154
171
  /** Current connection state. */
155
172
  get state(): PeerConnectionState;
@@ -189,10 +206,26 @@ export declare class PeerClient {
189
206
  */
190
207
  sendConfigReloadAndWait(msg: PeerConfigReload, timeoutMs?: number): Promise<PeerConfigReloadResponse | null>;
191
208
  /**
192
- * Send a peer.clusterSettings.request to the connected leader and wait for the
193
- * 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.
194
227
  *
195
- * @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
196
229
  * within the timeout — the caller keeps its current (config-default) settings.
197
230
  */
198
231
  sendClusterSettingsRequestAndWait(msg: PeerClusterSettingsRequest, timeoutMs?: number): Promise<PeerClusterSettingsResponse | null>;
@@ -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
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Which cluster-settings version each orchestrator in a cluster has applied,
3
+ * compared with the version of the shared `cluster_settings` row.
4
+ *
5
+ * A worker reports the version of the snapshot it pulled, which is the one its
6
+ * next spawn reads. A coordinator reports the version its settings reader
7
+ * currently serves. Pure, because `kici-admin cluster-settings show` imports
8
+ * it to validate and render the report the admin route builds.
9
+ */
10
+ import { z } from 'zod';
11
+ import type { PeerInfo } from './peer-registry.js';
12
+ export declare const settingsPropagationStatusSchema: z.ZodEnum<{
13
+ ahead: "ahead";
14
+ behind: "behind";
15
+ "in-sync": "in-sync";
16
+ }>;
17
+ export type SettingsPropagationStatus = z.infer<typeof settingsPropagationStatusSchema>;
18
+ export declare const SettingsPropagationStatus: {
19
+ ahead: "ahead";
20
+ behind: "behind";
21
+ "in-sync": "in-sync";
22
+ };
23
+ export declare const settingsPropagationRoleSchema: z.ZodEnum<{
24
+ coordinator: "coordinator";
25
+ worker: "worker";
26
+ }>;
27
+ export declare const settingsPropagationEntrySchema: z.ZodObject<{
28
+ instanceId: z.ZodString;
29
+ role: z.ZodEnum<{
30
+ coordinator: "coordinator";
31
+ worker: "worker";
32
+ }>;
33
+ self: z.ZodBoolean;
34
+ connected: z.ZodBoolean;
35
+ appliedVersion: z.ZodNumber;
36
+ status: z.ZodEnum<{
37
+ ahead: "ahead";
38
+ behind: "behind";
39
+ "in-sync": "in-sync";
40
+ }>;
41
+ lastHeartbeatAt: z.ZodNullable<z.ZodString>;
42
+ lastHeartbeatAgeMs: z.ZodNullable<z.ZodNumber>;
43
+ }, z.core.$loose>;
44
+ export type SettingsPropagationEntry = z.infer<typeof settingsPropagationEntrySchema>;
45
+ export declare const settingsPropagationReportSchema: z.ZodObject<{
46
+ currentVersion: z.ZodNumber;
47
+ reportedBy: z.ZodString;
48
+ generatedAt: z.ZodString;
49
+ orchestrators: z.ZodArray<z.ZodObject<{
50
+ instanceId: z.ZodString;
51
+ role: z.ZodEnum<{
52
+ coordinator: "coordinator";
53
+ worker: "worker";
54
+ }>;
55
+ self: z.ZodBoolean;
56
+ connected: z.ZodBoolean;
57
+ appliedVersion: z.ZodNumber;
58
+ status: z.ZodEnum<{
59
+ ahead: "ahead";
60
+ behind: "behind";
61
+ "in-sync": "in-sync";
62
+ }>;
63
+ lastHeartbeatAt: z.ZodNullable<z.ZodString>;
64
+ lastHeartbeatAgeMs: z.ZodNullable<z.ZodNumber>;
65
+ }, z.core.$loose>>;
66
+ }, z.core.$loose>;
67
+ export type SettingsPropagationReport = z.infer<typeof settingsPropagationReportSchema>;
68
+ /** The peer-registry fields a report reads. */
69
+ export type PropagationPeer = Pick<PeerInfo, 'instanceId' | 'role' | 'connected' | 'lastHeartbeatAt' | 'clusterSettingsVersion'>;
70
+ /**
71
+ * `ahead` means the row's version went backwards. A worker in that state never
72
+ * pulls again, because it pulls only when a coordinator advertises a higher
73
+ * version than its own.
74
+ */
75
+ export declare function classifySettingsVersion(applied: number, current: number): SettingsPropagationStatus;
76
+ export declare function buildSettingsPropagationReport(input: {
77
+ currentVersion: number;
78
+ self: {
79
+ instanceId: string;
80
+ appliedVersion: number;
81
+ };
82
+ peers: readonly PropagationPeer[];
83
+ now: number;
84
+ }): SettingsPropagationReport;
85
+ //# sourceMappingURL=settings-propagation.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The cluster-settings snapshot a coordinator serves to DB-less workers.
3
+ *
4
+ * A worker cannot read `cluster_settings`, so every knob it applies at spawn
5
+ * reaches it through this snapshot, pulled over the peer channel whenever a
6
+ * coordinator advertises a newer settings version. Each value is the stored
7
+ * override, or the serving coordinator's configured default when the column
8
+ * is NULL.
9
+ */
10
+ import type { WorkerClusterSettings } from '@kici-dev/engine';
11
+ import type { ClusterSettingsReader } from './cluster-settings-reader.js';
12
+ /** The configured defaults behind each snapshot field. */
13
+ export interface WorkerClusterSettingDefaults {
14
+ agentTokenTtlMs: number;
15
+ firecrackerApiSocketWaitMs: number;
16
+ }
17
+ /** A worker snapshot and the `cluster_settings.version` its values belong to. */
18
+ export interface WorkerClusterSettingsSnapshot {
19
+ version: number;
20
+ settings: WorkerClusterSettings;
21
+ }
22
+ /**
23
+ * Resolve the worker snapshot and its version from ONE read of the row. A
24
+ * worker stores the version with the values and pulls again only when a
25
+ * coordinator advertises a higher version, so a version read separately from
26
+ * the values could pair a newer version with older values for good.
27
+ */
28
+ export declare function resolveWorkerClusterSettingsSnapshot(reader: Pick<ClusterSettingsReader, 'getRow'>, defaults: WorkerClusterSettingDefaults): Promise<WorkerClusterSettingsSnapshot>;
29
+ //# sourceMappingURL=worker-cluster-settings.d.ts.map
package/dist/config.d.ts CHANGED
@@ -72,6 +72,7 @@ declare const configSchema: z.ZodObject<{
72
72
  globalEvalCandidateTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
73
73
  globalEvalCacheMax: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
74
74
  globalEvalWaitTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
75
+ firecrackerApiSocketWaitMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
75
76
  queueMaxDepth: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
76
77
  queueTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
77
78
  unroutableGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -104,6 +105,8 @@ declare const configSchema: z.ZodObject<{
104
105
  rerouteSpawnWindowMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
105
106
  rerouteAckTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
106
107
  rerouteMaxHops: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
108
+ rerouteSpawnMaxAttempts: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
109
+ rerouteSpawnRetryBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
107
110
  rerouteFlapGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
108
111
  scalerSpawnTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
109
112
  backupStalenessWarnHours: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -283,6 +286,7 @@ export declare const packagingConfigSchema: z.ZodObject<{
283
286
  globalEvalCandidateTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
284
287
  globalEvalCacheMax: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
285
288
  globalEvalWaitTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
289
+ firecrackerApiSocketWaitMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
286
290
  queueMaxDepth: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
287
291
  queueTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
288
292
  unroutableGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -315,6 +319,8 @@ export declare const packagingConfigSchema: z.ZodObject<{
315
319
  rerouteSpawnWindowMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
316
320
  rerouteAckTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
317
321
  rerouteMaxHops: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
322
+ rerouteSpawnMaxAttempts: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
323
+ rerouteSpawnRetryBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
318
324
  rerouteFlapGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
319
325
  scalerSpawnTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
320
326
  backupStalenessWarnHours: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -527,6 +533,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
527
533
  globalEvalCandidateTimeoutMs: number;
528
534
  globalEvalCacheMax: number;
529
535
  globalEvalWaitTimeoutMs: number;
536
+ firecrackerApiSocketWaitMs: number;
530
537
  queueMaxDepth: number;
531
538
  queueTimeoutMs: number;
532
539
  unroutableGraceMs: number;
@@ -556,6 +563,8 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
556
563
  rerouteSpawnWindowMs: number;
557
564
  rerouteAckTimeoutMs: number;
558
565
  rerouteMaxHops: number;
566
+ rerouteSpawnMaxAttempts: number;
567
+ rerouteSpawnRetryBackoffMs: number;
559
568
  rerouteFlapGraceMs: number;
560
569
  scalerSpawnTimeoutMs: number;
561
570
  backupStalenessWarnHours: number;
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ export declare function up(db: Kysely<unknown>): Promise<void>;
3
+ export declare function down(db: Kysely<unknown>): Promise<void>;
4
+ //# sourceMappingURL=154_cluster_settings_firecracker_api_socket_wait.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add the two per-org spawn-retry columns a worker applies to a rerouted job (both nullable):
4
+ *
5
+ * - `reroute_spawn_max_attempts INTEGER` — agent spawns a worker attempts for one rerouted
6
+ * job before it gives the job back to its coordinator. NULL falls back to the cluster
7
+ * default (`rerouteSpawnMaxAttempts`, 3).
8
+ * - `reroute_spawn_retry_backoff_ms BIGINT` — wait after a failed spawn before the next
9
+ * attempt. NULL falls back to `rerouteSpawnRetryBackoffMs` (5 s).
10
+ *
11
+ * Operators tune them per org via `kici-admin org-settings reroute set`.
12
+ *
13
+ * Idempotent: each add is guarded on column existence.
14
+ */
15
+ export declare function up(db: Kysely<unknown>): Promise<void>;
16
+ export declare function down(db: Kysely<unknown>): Promise<void>;
17
+ //# sourceMappingURL=155_org_settings_reroute_spawn_retry.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Record how the event router resolved each internal event:
4
+ *
5
+ * - `match_outcome TEXT` — `matched`, `buffered`, `no-registration`,
6
+ * `no-target-repo`, `trust-blocked` or `no-trigger-match`.
7
+ * - `matched_count INTEGER` — how many registrations the event was dispatched to.
8
+ *
9
+ * Both are written when the event is processed and are NULL before that, and
10
+ * for rows processed before these columns existed. Nullable with no default,
11
+ * so the ALTER is catalog-only on a large table.
12
+ *
13
+ * Idempotent: `ADD COLUMN IF NOT EXISTS`.
14
+ */
15
+ export declare function up(db: Kysely<unknown>): Promise<void>;
16
+ export declare function down(db: Kysely<unknown>): Promise<void>;
17
+ //# sourceMappingURL=156_kici_events_match_outcome.d.ts.map