@kici-dev/orchestrator 0.5.0 → 0.6.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 (149) hide show
  1. package/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
  2. package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
  3. package/dist/__test-helpers__/mock-db.d.ts +23 -0
  4. package/dist/agent/dispatcher.d.ts +81 -13
  5. package/dist/agent/host-roster.d.ts +37 -5
  6. package/dist/agent/registry.d.ts +2 -0
  7. package/dist/app.d.ts +33 -6
  8. package/dist/approvals/apply-decision.d.ts +67 -19
  9. package/dist/approvals/held-run-release-wiring.d.ts +45 -0
  10. package/dist/approvals/independent-wiring.d.ts +55 -0
  11. package/dist/approvals/resume-router.d.ts +9 -0
  12. package/dist/approvals/triggerer-subject.d.ts +37 -0
  13. package/dist/cache/dep-cache.d.ts +54 -10
  14. package/dist/cache/global-eval-round-cache.d.ts +7 -0
  15. package/dist/cli/api-client.d.ts +18 -0
  16. package/dist/cli/commands/cluster-settings.d.ts +11 -0
  17. package/dist/cli/commands/diagnose.d.ts +6 -0
  18. package/dist/cli/commands/held-run.d.ts +58 -0
  19. package/dist/cli/commands/trust-policy.d.ts +117 -12
  20. package/dist/cli/kici-admin.d.ts +10 -0
  21. package/dist/cli.js +2272 -1025
  22. package/dist/cluster/cluster-identity.d.ts +3 -3
  23. package/dist/cluster/cluster-settings-reader.d.ts +7 -1
  24. package/dist/cluster/coordinator.d.ts +44 -0
  25. package/dist/cluster/index.d.ts +4 -0
  26. package/dist/cluster/peer-handler.d.ts +19 -0
  27. package/dist/cluster/peer-registry.d.ts +17 -0
  28. package/dist/cluster/plan-headroom-store.d.ts +26 -0
  29. package/dist/cluster/worker-eviction.d.ts +69 -0
  30. package/dist/cold-store/load-event-log-range.d.ts +10 -3
  31. package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
  32. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
  33. package/dist/config.d.ts +21 -21
  34. package/dist/contexts/held-runs.d.ts +123 -16
  35. package/dist/contexts/protection/aggregate.d.ts +3 -2
  36. package/dist/contexts/protection/branch-gate.d.ts +15 -0
  37. package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
  38. package/dist/contexts/protection/pipeline.d.ts +18 -0
  39. package/dist/contexts/release-queued-holds.d.ts +35 -0
  40. package/dist/dashboard/attestation-filters.d.ts +8 -0
  41. package/dist/dashboard/handler.d.ts +6 -24
  42. package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
  43. package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
  44. package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
  45. package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
  46. package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
  47. package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
  48. package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
  49. package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
  50. package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
  51. package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
  52. package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
  53. package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
  54. package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
  55. package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
  56. package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
  57. package/dist/db/types.d.ts +368 -10
  58. package/dist/entry-helpers.d.ts +28 -0
  59. package/dist/events/event-emitter.d.ts +22 -0
  60. package/dist/events/event-router.d.ts +87 -1
  61. package/dist/events/types.d.ts +2 -23
  62. package/dist/git/credential-broker.d.ts +94 -0
  63. package/dist/git/inherited-secret.d.ts +24 -0
  64. package/dist/git/installation-token.d.ts +38 -0
  65. package/dist/git/job-context.d.ts +13 -0
  66. package/dist/index.js +324 -84
  67. package/dist/metrics/agent-metrics-aggregator.d.ts +2 -2
  68. package/dist/metrics/prometheus.d.ts +111 -3
  69. package/dist/oidc/oidc-mint-registration.d.ts +7 -3
  70. package/dist/orchestrator-core.d.ts +79 -23
  71. package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
  72. package/dist/pipeline/dispatch-matched-workflow.d.ts +302 -31
  73. package/dist/pipeline/global-eval-round.d.ts +14 -0
  74. package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
  75. package/dist/pipeline/invoke-gate.d.ts +164 -0
  76. package/dist/pipeline/needs-scheduler.d.ts +13 -0
  77. package/dist/pipeline/process-webhook.d.ts +119 -31
  78. package/dist/pipeline/processor.d.ts +226 -21
  79. package/dist/pipeline/rerun.d.ts +21 -0
  80. package/dist/pipeline/resume-workflow.d.ts +40 -7
  81. package/dist/pipeline/route-or-dispatch-jobs.d.ts +9 -0
  82. package/dist/pipeline/security-hold-check.d.ts +217 -0
  83. package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
  84. package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
  85. package/dist/policy/dashboard-write-policy.d.ts +14 -0
  86. package/dist/provider-registry.d.ts +10 -2
  87. package/dist/providers/github/check-status-poster.d.ts +11 -0
  88. package/dist/providers/github/index.d.ts +0 -2
  89. package/dist/providers/github/normalizer.d.ts +3 -2
  90. package/dist/providers/local/normalizer.d.ts +3 -5
  91. package/dist/providers/universal-git/index.d.ts +11 -2
  92. package/dist/queue/job-queue.d.ts +43 -1
  93. package/dist/registration/extractor.d.ts +3 -3
  94. package/dist/registration/registration-index.d.ts +7 -0
  95. package/dist/registration/registration-store.d.ts +22 -0
  96. package/dist/reporting/check-run-reporter.d.ts +94 -1
  97. package/dist/reporting/check-run-summary.d.ts +25 -1
  98. package/dist/reporting/execution-tracker.d.ts +207 -2
  99. package/dist/reporting/run-aggregator.d.ts +4 -14
  100. package/dist/reporting/step-display-order.d.ts +43 -0
  101. package/dist/routes/admin-held-runs.d.ts +142 -0
  102. package/dist/routes/admin-trust-policy.d.ts +34 -6
  103. package/dist/routes/admin.d.ts +9 -0
  104. package/dist/scaler/backend-factory.d.ts +55 -0
  105. package/dist/scaler/bare-metal-backend.d.ts +43 -5
  106. package/dist/scaler/claim-store.d.ts +119 -0
  107. package/dist/scaler/config.d.ts +4 -0
  108. package/dist/scaler/container-backend.d.ts +18 -4
  109. package/dist/scaler/container-routing.d.ts +23 -0
  110. package/dist/scaler/container-spawn.d.ts +28 -0
  111. package/dist/scaler/event-backend.d.ts +124 -0
  112. package/dist/scaler/event-provision-reaper.d.ts +350 -0
  113. package/dist/scaler/failure-tracker.d.ts +1 -1
  114. package/dist/scaler/firecracker-backend.d.ts +33 -6
  115. package/dist/scaler/index.d.ts +11 -1
  116. package/dist/scaler/label-matcher.d.ts +4 -3
  117. package/dist/scaler/manager.d.ts +899 -72
  118. package/dist/scaler/resolve-container-auth.d.ts +45 -0
  119. package/dist/scaler/scaler-events.d.ts +11 -0
  120. package/dist/scaler/scaler-state-store.d.ts +294 -10
  121. package/dist/scaler/types.d.ts +131 -13
  122. package/dist/scaler/warm-pool.d.ts +132 -29
  123. package/dist/security/comment-handler.d.ts +50 -11
  124. package/dist/security/identity-link.d.ts +60 -0
  125. package/dist/security/lock-source.d.ts +11 -13
  126. package/dist/security/reduced-privilege-note.d.ts +59 -0
  127. package/dist/security/trust-directory-store.d.ts +233 -0
  128. package/dist/security/trust-policy-gate.d.ts +94 -59
  129. package/dist/security/trust-policy-store.d.ts +3 -0
  130. package/dist/security/trust-resolver.d.ts +14 -98
  131. package/dist/server.d.ts +11 -1
  132. package/dist/server.js +17671 -7350
  133. package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
  134. package/dist/stale-detector/stale-run-detector.d.ts +61 -3
  135. package/dist/standalone.js +24238 -13822
  136. package/dist/worker/in-memory-job-queue.d.ts +1 -1
  137. package/dist/ws/agent-handler.d.ts +28 -4
  138. package/dist/ws/dashboard-context-handler.d.ts +28 -4
  139. package/dist/ws/dashboard-global-workflows-handler.d.ts +6 -0
  140. package/dist/ws/failure-messages.d.ts +2 -0
  141. package/dist/ws/git-credential-relay.d.ts +39 -0
  142. package/dist/ws/oidc-token-relay.d.ts +6 -11
  143. package/dist/ws/platform-client.d.ts +54 -9
  144. package/installer-image-digests.json +3 -3
  145. package/package.json +24 -19
  146. package/sbom.spdx.json +864 -822
  147. package/dist/approvals/team-membership-lookup.d.ts +0 -13
  148. package/dist/providers/github/contributor-resolver.d.ts +0 -30
  149. package/dist/security/contributor-cache.d.ts +0 -83
@@ -23,9 +23,9 @@ export interface ClusterIdentityDeps {
23
23
  */
24
24
  storagePrefix?: string;
25
25
  /**
26
- * E2E escape hatch — when true, validateS3Sentinel logs a warning and
27
- * returns early. Mirrors the orchestrator's
28
- * `config.skipS3SentinelValidation` (KICI_SKIP_S3_SENTINEL_VALIDATION).
26
+ * Test-only escape hatch — when true, validateS3Sentinel logs a warning and
27
+ * returns early. Supplied only by the build-time test double's injected
28
+ * fault-injection policy; the shipped orchestrator always validates.
29
29
  */
30
30
  skipSentinelValidation?: boolean;
31
31
  }
@@ -1,7 +1,9 @@
1
1
  import type { Kysely } from 'kysely';
2
2
  import type { Database } 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' | 'contributor_cache_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' | 'concurrency_wait_timeout_ms' | 'agent_token_ttl_ms' | 'ownership_db_check_timeout_ms' | 'unroutable_grace_ms' | 'ingest_overflow_claim_timeout_ms';
4
+ export type ClusterNumberColumn = 'max_github_payload_bytes' | 'event_log_max_payload_bytes' | 'lock_file_max_bytes' | 'webhook_dedup_ttl_ms'
5
+ /** @deprecated Readable, but no call site reads it. Removed at v1.0.0. */
6
+ | 'contributor_cache_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' | 'concurrency_wait_timeout_ms' | 'agent_token_ttl_ms' | 'ownership_db_check_timeout_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
7
  /** Text columns on cluster_settings readable via {@link ClusterSettingsReader}. */
6
8
  export type ClusterStringColumn = 'dashboard_verified_issuer';
7
9
  /** Boolean columns on cluster_settings readable via {@link ClusterSettingsReader}. */
@@ -39,6 +41,10 @@ export declare const CACHE_MAX_ENTRIES_CEILING = 100000;
39
41
  * A value that is not a usable positive count (NaN, non-finite, below 1) falls
40
42
  * back to the configured default rather than clamping to 1: a 1-entry cache
41
43
  * thrashes silently, which is harder to diagnose than simply ignoring garbage.
44
+ * The fallback is floored and bounded by the ceiling on the same path as the
45
+ * stored value, so neither a fractional nor an above-ceiling configured default
46
+ * can reach the LRU constructor as a non-integer or unclamped max — the
47
+ * constructor rejects both, and either would prevent boot.
42
48
  */
43
49
  export declare function clampCacheMaxEntries(value: number, fallback: number): number;
44
50
  /**
@@ -90,6 +90,8 @@ export interface RunCoordinatorDeps {
90
90
  sendAndWaitAckViaHandler?: (targetInstanceId: string, msg: JobReroute, timeoutMs: number) => Promise<boolean>;
91
91
  /** Fallback: send a message via server-side (incoming) peer connection (fire-and-forget). */
92
92
  sendToPeerViaHandler?: (targetInstanceId: string, msg: PeerToPeerMessage) => boolean;
93
+ /** Close a peer's incoming socket by instanceId (used by worker eviction). */
94
+ closePeerViaHandler?: (targetInstanceId: string, code: number, reason: string) => void;
93
95
  ackTimeoutMs?: number;
94
96
  /** Stale peer timeout in ms. Default: 60000 (60s). */
95
97
  staleTimeoutMs?: number;
@@ -137,7 +139,23 @@ export declare class RunCoordinator {
137
139
  private readonly nakTracker;
138
140
  /** Stale eviction timer handle. */
139
141
  private staleEvictionTimer;
142
+ /** Plan-ceiling worker eviction (drain-then-disconnect). */
143
+ private readonly workerEviction;
144
+ /** Per-peer "no in-flight rerouted jobs" callbacks, fired by worker drain. */
145
+ private readonly evictionIdleCallbacks;
146
+ private readonly closePeerViaHandler?;
140
147
  constructor(deps: RunCoordinatorDeps);
148
+ /** In-flight rerouted jobs currently attributed to a peer. */
149
+ private runningJobCountForPeer;
150
+ /** Fire a registered drain idle-callback once the peer has no in-flight jobs. */
151
+ private fireIdleIfDrained;
152
+ /**
153
+ * Reconcile this coordinator's workers against the pushed ceiling. Invoked on
154
+ * every `plan.headroom` frame: when `evictExcess` is set, the newest workers
155
+ * past `ceiling` are drained; otherwise any in-flight drain is cancelled and
156
+ * the worker restored, so a raised ceiling rescues it.
157
+ */
158
+ reconcileWorkerEviction(ceiling: number, evictExcess: boolean): void;
141
159
  /**
142
160
  * Route jobs for a webhook run. Claims jobs that local agents can handle,
143
161
  * reroutes the rest to peers with matching capacity.
@@ -268,6 +286,32 @@ export declare class RunCoordinator {
268
286
  * its worker is connected.
269
287
  */
270
288
  private trackReroutedJob;
289
+ /** Arm a spawn-window / reap timer that fires {@link handleRerouteSpawnTimeout}. */
290
+ private armRerouteWindow;
291
+ /**
292
+ * Whether the reroute target has actually started the job, read from the
293
+ * shared `execution_jobs` row.
294
+ *
295
+ * A read failure answers "not running" so a transient database fault cannot
296
+ * silently disable the backstop — the re-dispatch it then performs is the
297
+ * behavior that shipped before this check existed, and it is idempotent.
298
+ * Only the FIRST observation goes through this path: once `peerStarted`
299
+ * latches, a read fault can no longer bounce a running job.
300
+ */
301
+ private rerouteTargetIsRunning;
302
+ /**
303
+ * The peer owns the job, so the backstop must not re-dispatch it. Release the
304
+ * tracking entry once the shared row is terminal, otherwise re-arm the window
305
+ * as a reap poll.
306
+ *
307
+ * The entry cannot simply be dropped here: cancel propagation still needs it
308
+ * while the job runs. It also cannot simply be kept, because a peer
309
+ * COORDINATOR relays no terminal `job.progress` — the shared row IS its
310
+ * report — so nothing else would ever release it and `reroutedJobs` would
311
+ * grow for the lifetime of the process. A read fault re-arms (never releases),
312
+ * so a transient fault costs one more poll rather than tracking.
313
+ */
314
+ private reapOrDeferRerouteTracking;
271
315
  /**
272
316
  * Post-ACK spawn-window backstop. Fires when a rerouted job's peer accepted
273
317
  * but produced no progress within the window (Layer A), or immediately when a
@@ -21,6 +21,10 @@ export type { RaftPersistentState } from './raft-state.js';
21
21
  export { OrphanRecovery } from './orphan-recovery.js';
22
22
  export { RunCoordinator } from './coordinator.js';
23
23
  export type { RunContext, JobToRoute, RouteResult } from './coordinator.js';
24
+ export { PlanHeadroomStore } from './plan-headroom-store.js';
25
+ export type { StoredPlanHeadroom } from './plan-headroom-store.js';
26
+ export { WorkerEviction, selectEvictionTargets, WORKER_DRAIN_TIMEOUT_MS, } from './worker-eviction.js';
27
+ export type { EvictionHooks } from './worker-eviction.js';
24
28
  export { createClusterHealthRoutes } from './health-api.js';
25
29
  export type { ClusterHealthRoutesDeps } from './health-api.js';
26
30
  //# sourceMappingURL=index.d.ts.map
@@ -33,6 +33,14 @@ export interface PeerHandlerDeps {
33
33
  peerRegistry: PeerRegistry;
34
34
  /** Callback to get this orchestrator's local agent inventory for heartbeats. */
35
35
  getLocalInventory: () => Omit<PeerHeartbeat, 'type'>;
36
+ /**
37
+ * The Platform-pushed worker ceiling this coordinator enforces, or `null`
38
+ * when none was ever received (admit worker joins freely). Read at each
39
+ * worker-peer admission. Served from the persisted PlanHeadroomStore, so it
40
+ * survives a Platform disconnect — the coordinator keeps enforcing the last
41
+ * known ceiling rather than resetting to unlimited.
42
+ */
43
+ getWorkerCeiling?: () => Promise<number | null>;
36
44
  /** Heartbeat interval in ms. Default: 30000 (30s). */
37
45
  heartbeatIntervalMs?: number;
38
46
  /** Auth timeout in ms. Default: 15000 (15s). */
@@ -103,9 +111,20 @@ export interface PeerHandlerDeps {
103
111
  * Returns `handleConnection(ws, remoteIp?)` which should be called when a new
104
112
  * WebSocket connection is upgraded on the peer endpoint.
105
113
  */
114
+ /**
115
+ * Whether a joining peer may be admitted against the plan ceiling.
116
+ *
117
+ * Only workers are gated: a coordinator peer holds its own Platform connection
118
+ * and is counted there, and a peer advertising no role registers as a
119
+ * coordinator (see PeerRegistry.addPeer), so neither is gated here. A `null`
120
+ * ceiling — none ever received — admits freely. The ceiling is ABSOLUTE, so the
121
+ * join is admitted only while the currently-connected worker count is below it.
122
+ */
123
+ export declare function shouldAdmitWorker(role: string | undefined, ceiling: number | null, connectedWorkerCount: number): boolean;
106
124
  export declare function createPeerHandler(deps: PeerHandlerDeps): {
107
125
  handleConnection: (ws: PeerWsLike, remoteIp?: string) => void;
108
126
  sendToPeer: (targetInstanceId: string, msg: PeerToPeerMessage) => boolean;
127
+ closePeer: (targetInstanceId: string, code: number, reason: string) => void;
109
128
  sendAndWaitAck: (targetInstanceId: string, msg: JobReroute, timeoutMs?: number) => Promise<boolean>;
110
129
  sendConfigReloadAndWait: (targetInstanceId: string, msg: PeerConfigReload, timeoutMs?: number) => Promise<PeerConfigReloadResponse | null>;
111
130
  sendLogsCollectAndWait: (targetInstanceId: string, msg: PeerLogsCollectRequest, timeoutMs: number) => Promise<Buffer>;
@@ -35,6 +35,8 @@ export interface PeerInfo {
35
35
  routingKeys: string[];
36
36
  connected: boolean;
37
37
  lastHeartbeatAt: number;
38
+ /** When this peer first connected. Distinct from `lastHeartbeatAt`, which moves; this does not. */
39
+ connectedAt: number;
38
40
  agents: PeerAgentInfo[];
39
41
  draining: boolean;
40
42
  capabilities: PeerCapabilities;
@@ -71,6 +73,12 @@ export interface PeerRegistryOptions {
71
73
  onClusterSettingsVersionBehind?: (peerVersion: number) => void;
72
74
  /** Called when a peer transitions to disconnected state. */
73
75
  onPeerDisconnected?: (instanceId: string) => void;
76
+ /**
77
+ * Called when the connected-peer set changes (a fresh add or a
78
+ * markDisconnected), so a coordinator can re-report its `cluster.membership`
79
+ * snapshot to the Platform without waiting for the periodic interval.
80
+ */
81
+ onMembershipChange?: () => void;
74
82
  }
75
83
  export declare class PeerRegistry {
76
84
  private readonly peers;
@@ -81,6 +89,7 @@ export declare class PeerRegistry {
81
89
  private readonly onRegistryVersionBehind?;
82
90
  private readonly onClusterSettingsVersionBehind?;
83
91
  private readonly onPeerDisconnected?;
92
+ private readonly onMembershipChange?;
84
93
  constructor(options?: PeerRegistryOptions);
85
94
  /**
86
95
  * Update the local config version for comparison with peer heartbeats.
@@ -196,6 +205,14 @@ export declare class PeerRegistry {
196
205
  * Get all peers with role=worker.
197
206
  */
198
207
  getWorkerPeers(): PeerInfo[];
208
+ /**
209
+ * Currently-connected worker peers.
210
+ *
211
+ * A peer that advertises no role registers as a coordinator (see `addPeer`),
212
+ * so a worker predating the role field is not counted here. That under-counts,
213
+ * which errs permissive, and it self-heals as workers upgrade.
214
+ */
215
+ getConnectedWorkerPeers(): PeerInfo[];
199
216
  /**
200
217
  * Get all peers with role=coordinator.
201
218
  */
@@ -0,0 +1,26 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { PlanHeadroom } from '@kici-dev/engine';
3
+ import type { Database } from '../db/types.js';
4
+ /** The ceiling as last pushed by the Platform. */
5
+ export interface StoredPlanHeadroom {
6
+ maxWorkerPeers: number;
7
+ orgLimit: number;
8
+ orgTotal: number;
9
+ evictExcess: boolean;
10
+ updatedAt: Date;
11
+ }
12
+ /**
13
+ * The orchestrator's cache of the Platform-owned worker ceiling.
14
+ *
15
+ * Persisted so a coordinator restarting during a Platform outage keeps
16
+ * enforcing the last known ceiling instead of resetting to unlimited. Same
17
+ * ownership shape as `org_trust_policy`: the Platform writes it, the
18
+ * orchestrator only reads it back.
19
+ */
20
+ export declare class PlanHeadroomStore {
21
+ private readonly db;
22
+ constructor(db: Kysely<Database>);
23
+ read(): Promise<StoredPlanHeadroom | null>;
24
+ write(headroom: PlanHeadroom): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=plan-headroom-store.d.ts.map
@@ -0,0 +1,69 @@
1
+ import type { PeerInfo, PeerRegistry } from './peer-registry.js';
2
+ /** How long a draining worker may hold in-flight jobs before it is disconnected anyway. */
3
+ export declare const WORKER_DRAIN_TIMEOUT_MS: number;
4
+ /**
5
+ * The coordinator-side operations eviction needs.
6
+ *
7
+ * Declared here rather than reached for on the coordinator directly: the
8
+ * coordinator owns dispatch and job attribution, but exposes no ineligibility
9
+ * or idle-notification surface, and widening its public API for one caller
10
+ * would couple eviction to it. A fake implementation makes this class unit
11
+ * testable without a running cluster.
12
+ */
13
+ export interface EvictionHooks {
14
+ /** Stop rerouting new jobs to this peer. */
15
+ markIneligible(instanceId: string): void;
16
+ /**
17
+ * Restore this peer to dispatch eligibility and drop any idle-notification
18
+ * registration. Called when a drain is cancelled (the ceiling rose so the
19
+ * peer is no longer excess) and after a drained peer is disconnected, so a
20
+ * later re-admission of the same instance starts dispatch-eligible.
21
+ */
22
+ clearIneligible(instanceId: string): void;
23
+ /** In-flight rerouted jobs currently attributed to this peer. */
24
+ runningJobCount(instanceId: string): number;
25
+ /** Invoke the callback once this peer has no in-flight rerouted jobs. */
26
+ onPeerIdle(instanceId: string, callback: () => void): void;
27
+ /** Close this peer's connection with the given code and reason. */
28
+ closePeer(instanceId: string, code: number, reason: string): void;
29
+ }
30
+ /**
31
+ * The workers to drain to bring this coordinator down to its ceiling.
32
+ *
33
+ * Newest-first by connect time: the joins that crossed the line are the ones
34
+ * that go, so long-running capacity is not shuffled arbitrarily.
35
+ */
36
+ export declare function selectEvictionTargets(workers: readonly PeerInfo[], ceiling: number): PeerInfo[];
37
+ /**
38
+ * Drain-then-disconnect this coordinator's excess workers.
39
+ *
40
+ * A target is first marked ineligible for new dispatch, then closed once its
41
+ * in-flight rerouted jobs reach terminal status — which the worker's durable
42
+ * outbox and `job.progress.ack` already guarantee — or after
43
+ * `WORKER_DRAIN_TIMEOUT_MS`, after which the coordinator's existing
44
+ * orphan-recovery guard handles whatever remains.
45
+ */
46
+ export declare class WorkerEviction {
47
+ private readonly peerRegistry;
48
+ private readonly hooks;
49
+ private readonly logger;
50
+ private readonly draining;
51
+ constructor(peerRegistry: PeerRegistry, hooks: EvictionHooks, logger: {
52
+ warn: (msg: string, meta?: unknown) => void;
53
+ });
54
+ /**
55
+ * Reconcile the connected worker set against the ceiling.
56
+ *
57
+ * When `evictExcess` is true, drain the newest workers past `ceiling`;
58
+ * otherwise the target set is empty. Any drain in flight for a worker that is
59
+ * no longer a target is CANCELLED — its timer cleared and its dispatch
60
+ * eligibility restored — so a raised ceiling (or the Platform clearing
61
+ * `evictExcess` once the org drops back under) rescues a worker instead of
62
+ * disconnecting it at the drain timeout.
63
+ */
64
+ reconcile(ceiling: number, evictExcess: boolean): void;
65
+ /** Stop an in-flight drain and restore the worker to dispatch eligibility. */
66
+ private cancelDrain;
67
+ private startDrain;
68
+ }
69
+ //# sourceMappingURL=worker-eviction.d.ts.map
@@ -13,10 +13,17 @@
13
13
  * dashboard's payload-detail view continues to resolve archived
14
14
  * deliveries identically to hot ones.
15
15
  */
16
- import { type Kysely, type Selectable } from 'kysely';
16
+ import { type Kysely } from 'kysely';
17
17
  import { type ColdStore } from '@kici-dev/shared';
18
- import type { Database, EventLogTable } from '../db/types.js';
19
- export type EventLogColdStoreRow = Selectable<EventLogTable>;
18
+ import type { Database } from '../db/types.js';
19
+ import type { EventLogColdStoreRow } from './tables/event-log.js';
20
+ /**
21
+ * Re-exported, not re-declared: the alias is defined beside the cold-store
22
+ * adapter that owns the table, and a second identical declaration here is what
23
+ * a homonym sweep flags. Kept as an export so this module's public surface is
24
+ * unchanged for its consumers.
25
+ */
26
+ export type { EventLogColdStoreRow };
20
27
  export interface LoadOrchEventLogRangeArgs {
21
28
  db: Kysely<Database>;
22
29
  coldStore: ColdStore | undefined;
@@ -15,10 +15,11 @@
15
15
  * rows after streaming. This is correct because cold chunks are
16
16
  * partitioned by `(routing_key, day)`, not by the secondary filters.
17
17
  */
18
- import { type Kysely, type Selectable } from 'kysely';
18
+ import { type Kysely } from 'kysely';
19
19
  import { type ColdStore } from '@kici-dev/shared';
20
- import type { Database, SecretAuditLogTable } from '../db/types.js';
21
- export type SecretAuditLogRow = Selectable<SecretAuditLogTable>;
20
+ import type { Database, SecretAuditLogRow } from '../db/types.js';
21
+ /** Re-exported rather than re-declared — see the note in `load-event-log-range`. */
22
+ export type { SecretAuditLogRow };
22
23
  export interface LoadSecretAuditLogRangeArgs {
23
24
  db: Kysely<Database>;
24
25
  coldStore: ColdStore | undefined;
@@ -13,10 +13,11 @@
13
13
  * synthetic prefix `__orchestrator__`. Mirrors the same pattern already
14
14
  * used for `event_log` payload keys when the routing key is missing.
15
15
  */
16
- import { type Kysely, type Selectable } from 'kysely';
16
+ import { type Kysely } from 'kysely';
17
17
  import { type ChunkCommitMetadata, type ColdRetention, type ColdStoreTableConfig, type EligiblePartition, type TableAdapter } from '@kici-dev/shared';
18
- import type { Database, SecretAuditLogTable } from '../../db/types.js';
19
- export type SecretAuditLogRow = Selectable<SecretAuditLogTable>;
18
+ import type { Database, SecretAuditLogRow } from '../../db/types.js';
19
+ /** Re-exported rather than re-declared — see the note in `load-event-log-range`. */
20
+ export type { SecretAuditLogRow };
20
21
  export interface SecretAuditLogAdapterOptions {
21
22
  overrides?: Partial<ColdStoreTableConfig>;
22
23
  }
package/dist/config.d.ts CHANGED
@@ -97,6 +97,13 @@ declare const configSchema: z.ZodObject<{
97
97
  scalerSpawnTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
98
98
  backupStalenessWarnHours: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
99
99
  scalerPendingSweepIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
100
+ scalerReapIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
101
+ scalerReapStrandedTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
102
+ scalerReapReattemptIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
103
+ scalerClaimRetentionMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
104
+ scalerProvisionBackoffBaseMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
105
+ scalerProvisionBackoffMaxMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
106
+ scalerProvisionMaxConsecutiveFailures: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
100
107
  cacheStorageType: z.ZodOptional<z.ZodEnum<{
101
108
  filesystem: "filesystem";
102
109
  s3: "s3";
@@ -177,12 +184,6 @@ declare const configSchema: z.ZodObject<{
177
184
  eventRouterRetryBaseBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
178
185
  eventRouterRetryMaxBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
179
186
  eventRouterRetryScanIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
180
- testMode: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
181
- testEventFailFirstN: z.ZodOptional<z.ZodString>;
182
- testMintDeferAudience: z.ZodOptional<z.ZodString>;
183
- testMintRejectAudience: z.ZodOptional<z.ZodString>;
184
- testRerunDelayMs: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
185
- testOmitDashboardRequestTypes: z.ZodOptional<z.ZodString>;
186
187
  eventLogMaxPayloadBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
187
188
  maxGithubPayloadBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
188
189
  lockFileMaxBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -202,7 +203,6 @@ declare const configSchema: z.ZodObject<{
202
203
  }>>;
203
204
  autoMigrate: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
204
205
  agentMaxReconnectDelayMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
205
- skipS3SentinelValidation: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
206
206
  otelExporterOtlpEndpoint: z.ZodOptional<z.ZodString>;
207
207
  clusterName: z.ZodOptional<z.ZodString>;
208
208
  cluster: z.ZodPrefault<z.ZodObject<{
@@ -300,6 +300,13 @@ export declare const packagingConfigSchema: z.ZodObject<{
300
300
  scalerSpawnTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
301
301
  backupStalenessWarnHours: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
302
302
  scalerPendingSweepIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
303
+ scalerReapIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
304
+ scalerReapStrandedTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
305
+ scalerReapReattemptIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
306
+ scalerClaimRetentionMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
307
+ scalerProvisionBackoffBaseMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
308
+ scalerProvisionBackoffMaxMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
309
+ scalerProvisionMaxConsecutiveFailures: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
303
310
  cacheStorageType: z.ZodOptional<z.ZodEnum<{
304
311
  filesystem: "filesystem";
305
312
  s3: "s3";
@@ -380,12 +387,6 @@ export declare const packagingConfigSchema: z.ZodObject<{
380
387
  eventRouterRetryBaseBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
381
388
  eventRouterRetryMaxBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
382
389
  eventRouterRetryScanIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
383
- testMode: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
384
- testEventFailFirstN: z.ZodOptional<z.ZodString>;
385
- testMintDeferAudience: z.ZodOptional<z.ZodString>;
386
- testMintRejectAudience: z.ZodOptional<z.ZodString>;
387
- testRerunDelayMs: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
388
- testOmitDashboardRequestTypes: z.ZodOptional<z.ZodString>;
389
390
  eventLogMaxPayloadBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
390
391
  maxGithubPayloadBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
391
392
  lockFileMaxBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -405,7 +406,6 @@ export declare const packagingConfigSchema: z.ZodObject<{
405
406
  }>>;
406
407
  autoMigrate: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
407
408
  agentMaxReconnectDelayMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
408
- skipS3SentinelValidation: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
409
409
  otelExporterOtlpEndpoint: z.ZodOptional<z.ZodString>;
410
410
  clusterName: z.ZodOptional<z.ZodString>;
411
411
  cluster: z.ZodPrefault<z.ZodObject<{
@@ -533,6 +533,13 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
533
533
  scalerSpawnTimeoutMs: number;
534
534
  backupStalenessWarnHours: number;
535
535
  scalerPendingSweepIntervalMs: number;
536
+ scalerReapIntervalMs: number;
537
+ scalerReapStrandedTimeoutMs: number;
538
+ scalerReapReattemptIntervalMs: number;
539
+ scalerClaimRetentionMs: number;
540
+ scalerProvisionBackoffBaseMs: number;
541
+ scalerProvisionBackoffMaxMs: number;
542
+ scalerProvisionMaxConsecutiveFailures: number;
536
543
  cacheStorageType?: "filesystem" | "s3" | undefined;
537
544
  cacheStoragePath?: string | undefined;
538
545
  cacheStorageS3Bucket?: string | undefined;
@@ -595,12 +602,6 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
595
602
  eventRouterRetryBaseBackoffMs: number;
596
603
  eventRouterRetryMaxBackoffMs: number;
597
604
  eventRouterRetryScanIntervalMs: number;
598
- testMode: boolean;
599
- testEventFailFirstN?: string | undefined;
600
- testMintDeferAudience?: string | undefined;
601
- testMintRejectAudience?: string | undefined;
602
- testRerunDelayMs?: number | undefined;
603
- testOmitDashboardRequestTypes?: string | undefined;
604
605
  eventLogMaxPayloadBytes: number;
605
606
  maxGithubPayloadBytes: number;
606
607
  lockFileMaxBytes: number;
@@ -611,7 +612,6 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
611
612
  nodeEnv: "development" | "production" | "test";
612
613
  autoMigrate: boolean;
613
614
  agentMaxReconnectDelayMs: number;
614
- skipS3SentinelValidation: boolean;
615
615
  otelExporterOtlpEndpoint?: string | undefined;
616
616
  clusterName?: string | undefined;
617
617
  cluster: {
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * Manages the lifecycle: pending -> approved/rejected/expired.
5
5
  */
6
- import { Kysely } from 'kysely';
6
+ import { Kysely, type Transaction } from 'kysely';
7
7
  import { z } from 'zod';
8
8
  import { type ApprovalRequirement, type ApproverClause, type StepApprovalPayload, ApprovalDecision, HoldScope, TriggerSource } from '@kici-dev/engine';
9
9
  import type { Database, HeldRun, HeldRunApproval } from '../db/types.js';
10
+ /** A Kysely root handle or an in-flight transaction — query builders accept either. */
11
+ type Executor = Kysely<Database> | Transaction<Database>;
10
12
  /** Status values for held runs (held_runs table). */
11
13
  export declare enum HeldRunStatus {
12
14
  Pending = "pending",
@@ -17,7 +19,25 @@ export declare enum HeldRunStatus {
17
19
  }
18
20
  /**
19
21
  * Reason a run was held in the security queue. Persisted verbatim in
20
- * `held_runs.reason` and switched on by `buildSecurityHoldSummary`.
22
+ * `held_runs.reason` and whose vocabulary `buildSecurityHoldSummary` switches
23
+ * on.
24
+ *
25
+ * Each value names why a run sits in the queue. The enum stays whole because
26
+ * compiling code still names all four: `buildSecurityHoldSummary` renders one
27
+ * branch per value. Dropping a member breaks that, and breaks the parity test
28
+ * that pins `SECURITY_HOLD_JOB_IDS`' three policy-reason keys to this enum's
29
+ * options — the engine constant cannot import the enum, so that coupling lives
30
+ * only in the orchestrator's test. Nothing validates `held_runs.reason` against
31
+ * this enum, so a stored value is not rejected anywhere.
32
+ *
33
+ * - `fork_pr` — the org trust policy's fork switch held the run.
34
+ * - `context_trust` — a context's minimum-trust gate held the run.
35
+ * - `workflow_modification` — deprecated; no longer raised. Modifications to
36
+ * `.kici/` are surfaced on their own informational check and no longer feed
37
+ * a policy arm. Removed at v1.0.0.
38
+ * - `unknown_contributor` — deprecated; no longer raised. The policy turns on
39
+ * whether the pull request came from a fork, not on who opened it. Removed
40
+ * at v1.0.0.
21
41
  */
22
42
  export declare const SecurityHoldReason: z.ZodEnum<{
23
43
  context_trust: "context_trust";
@@ -37,6 +57,20 @@ export interface CreateHeldRunData {
37
57
  expiresAt: Date;
38
58
  /** Queue type: 'context' (default) or 'security'. */
39
59
  queueType?: 'context' | 'security';
60
+ /**
61
+ * Granularity of the held element. Omit to leave the column at its `'job'`
62
+ * default. The org trust policy's PR-wide hold passes `'workflow'`: it fires
63
+ * before any job is materialized and resumes by rebuilding the whole workflow
64
+ * dispatch, so `routeRelease` must send it to the workflow resume path.
65
+ */
66
+ scope?: HoldScope;
67
+ /**
68
+ * What triggered the hold. Omit to leave the column at its `'context'`
69
+ * default. Written explicitly by the trust-policy hold so its release signal
70
+ * carries the pair `routeRelease` discriminates on rather than relying on a
71
+ * column default to supply half of it.
72
+ */
73
+ triggerSource?: TriggerSource;
40
74
  }
41
75
  /**
42
76
  * Data required to create a generalized approval hold. Unlike the legacy
@@ -109,14 +143,57 @@ export interface ListHeldRunsOptions {
109
143
  export declare class HeldRunStore {
110
144
  private readonly db;
111
145
  constructor(db: Kysely<Database>);
112
- /** Create a new held run with pending status. */
113
- create(orgId: string, data: CreateHeldRunData): Promise<HeldRun>;
146
+ /**
147
+ * Create a new held run with pending status.
148
+ *
149
+ * `exec` is the executor the INSERT runs through, defaulting to the store's
150
+ * own connection. A caller that writes something the row cannot exist without
151
+ * — the job's pending dispatch context, without which the hold can never be
152
+ * resumed — passes its enclosing transaction, so the two land or roll back
153
+ * together. Handed in rather than taken from an ambient scope: Kysely has no
154
+ * such scope, so a `this.db` insert inside a `db.transaction()` callback runs
155
+ * on a different connection and commits on its own.
156
+ */
157
+ create(orgId: string, data: CreateHeldRunData, exec?: Executor): Promise<HeldRun>;
114
158
  /**
115
159
  * Create a generalized approval hold (workflow/job/step scope, explicit or
116
160
  * context trigger) carrying a normalized `ApprovalRequirement`. Returns
117
161
  * the created row.
162
+ *
163
+ * `exec` carries the same meaning it does on {@link create}.
164
+ */
165
+ createHold(orgId: string, data: CreateHoldData, exec?: Executor): Promise<HeldRun>;
166
+ /**
167
+ * Record that the pending `KiCI Security` check reached the provider, for
168
+ * every hold that gates it, so the settle that ends one knows it has a check
169
+ * to terminalize.
170
+ *
171
+ * Written AFTER the post returns, never before. The two orders fail
172
+ * differently and the failures are not equivalent: recording first and dying
173
+ * before the post leaves a row claiming a check the commit does not have, and
174
+ * the settle then CREATES one — a completed `KiCI Security` run appearing on a
175
+ * commit nothing ever held. Recording second leaves the opposite residue, a
176
+ * real pending check the settle declines to close, which is the same stuck
177
+ * check the fire-and-forget post could already produce. A fabricated failing
178
+ * check on a pull request is worse than a stuck one, so the write goes last.
179
+ *
180
+ * **One statement for all of them, not one per hold.** A commit's check run is
181
+ * shared by every hold on it, and the settle asks the contention query which
182
+ * of them still owns it. Marking them in a loop admits a PARTIAL mark: mark
183
+ * the reviewer hold, fail on the security hold, and the security row keeps
184
+ * `posted_pending_check: false` — so it is not counted as a contender, and
185
+ * approving the reviewer hold terminalizes the shared check `success` while
186
+ * the trust hold still gates the job. That is a fabricated PASSING check,
187
+ * which is the worse direction, reached without any process dying: a
188
+ * deadlock, a statement timeout or a lost connection between the two UPDATEs
189
+ * is enough. A single `WHERE id IN (…)` either marks every hold or none.
190
+ *
191
+ * The residual window is therefore one statement issued immediately after the
192
+ * provider call returns — narrow, and not only reachable by a process death,
193
+ * which is why the caller logs its failure rather than treating it as
194
+ * impossible.
118
195
  */
119
- createHold(orgId: string, data: CreateHoldData): Promise<HeldRun>;
196
+ markPendingCheckPosted(orgId: string, heldRunIds: readonly string[]): Promise<void>;
120
197
  /** INSERT one decision row using the given executor (root or transaction). */
121
198
  private insertDecisionRow;
122
199
  /** Flip a pending hold to 'approved' using the given executor. Undefined if not pending. */
@@ -190,20 +267,49 @@ export declare class HeldRunStore {
190
267
  */
191
268
  listOverdue(): Promise<HeldRun[]>;
192
269
  /**
193
- * Release overdue workflow timer holds. The install-gate wait action pauses
194
- * the workflow as a held run; on timer expiry it must RESUME (not fail like
195
- * a reviewer-hold expiry). Flips each overdue pending `hold_type` timer,
196
- * `hold_scope='workflow'` row to `released` and returns a `ReleaseSignal`
197
- * per row so the caller can resume the workflow. Runs BEFORE
198
- * `expireOverdue()` so these rows leave the pending pool before the
199
- * expire-and-fail sweep sees them.
270
+ * Release overdue timer holds at ANY scope. A wait action pauses its element
271
+ * as a held run; on timer expiry it must RESUME (not fail like a reviewer-hold
272
+ * expiry). Flips each overdue pending timer row to `released` and returns a
273
+ * `ReleaseSignal` per row, carrying the row's own scope so the caller can
274
+ * route it `routeRelease` sends a workflow-scoped one to the install-gate
275
+ * rebuild and a job-scoped one to the job re-dispatch path.
276
+ *
277
+ * Runs BEFORE `expireOverdue()` so these rows leave the pending pool before
278
+ * the expire-and-fail sweep sees them. That ordering is load-bearing, not
279
+ * incidental: `expireOverdue` is not scope-filtered, so a released-but-not-yet-
280
+ * resumed row would otherwise be expired out from under its resume.
200
281
  *
201
- * The filter matches every persisted spelling of the timer hold type, so a
202
- * row an un-upgraded orchestrator wrote as `wait_timer` still resumes rather
203
- * than falling through to the expire-and-fail sweep. `hold_scope` is what
204
- * keeps job-scoped dispatch-gate timer holds out of this sweep.
282
+ * The filter matches every persisted spelling of the timer hold type, so a row
283
+ * an un-upgraded orchestrator wrote as `wait_timer` still resumes rather than
284
+ * falling through to the expire-and-fail sweep. It deliberately does NOT
285
+ * filter on `hold_scope`: a job-scoped timer hold used to be excluded here,
286
+ * which left it with no release path at all — created, never released,
287
+ * eventually expired, its job never dispatched.
205
288
  */
206
289
  releaseDueWaitHolds(): Promise<ReleaseSignal[]>;
290
+ /**
291
+ * Every pending queued (concurrency) hold, with the org and concurrency group
292
+ * it belongs to — the input to the periodic release sweep, which needs to know
293
+ * WHICH groups have someone waiting before it looks up any limits.
294
+ *
295
+ * Returns the pair rather than full rows: the sweep only groups by it, and
296
+ * `listQueuedHoldsForContext` fetches the rows it actually releases.
297
+ */
298
+ listAllQueuedHolds(): Promise<Array<{
299
+ orgId: string | null;
300
+ concurrencyGroup: string | null;
301
+ }>>;
302
+ /**
303
+ * List the pending queued (concurrency) holds for one context's concurrency
304
+ * group, oldest first.
305
+ *
306
+ * Joined to `execution_runs` and filtered by `customer_id` as well as
307
+ * `context`: a context NAME is not unique across tenants, so without the org
308
+ * predicate one org's completing job could release another org's queued hold.
309
+ * Oldest-first is the release order — a queue that released newest-first would
310
+ * starve whoever waited longest.
311
+ */
312
+ listQueuedHoldsForContext(orgId: string, concurrencyGroup: string): Promise<HeldRun[]>;
207
313
  /**
208
314
  * Expire overdue pending runs. Called by the stale detector.
209
315
  * Sets status to 'expired' and resolved_at to now() for all
@@ -224,4 +330,5 @@ export declare function createHeldRunStoreFromUrl(databaseUrl: string, opts?: {
224
330
  store: HeldRunStore;
225
331
  dispose: () => Promise<void>;
226
332
  };
333
+ export {};
227
334
  //# sourceMappingURL=held-runs.d.ts.map