@kici-dev/orchestrator 0.12.0 → 0.13.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 (46) hide show
  1. package/dist/agent/dispatcher.d.ts +22 -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 +50 -0
  8. package/dist/cancel/sweep-stuck-cancelling.d.ts +3 -1
  9. package/dist/cli/commands/local-hook.d.ts +3 -2
  10. package/dist/cli/commands/local-trigger.d.ts +7 -0
  11. package/dist/cli.js +983 -683
  12. package/dist/cluster/coordinator-credential.d.ts +45 -0
  13. package/dist/cluster/coordinator.d.ts +18 -2
  14. package/dist/cluster/index.d.ts +1 -0
  15. package/dist/cluster/instance-heartbeat.d.ts +6 -1
  16. package/dist/cluster/join-token.d.ts +2 -0
  17. package/dist/cluster/peer-auth-coordinator.d.ts +13 -0
  18. package/dist/cluster/peer-client.d.ts +4 -0
  19. package/dist/cluster/peer-credentials.d.ts +31 -0
  20. package/dist/config.d.ts +3 -0
  21. package/dist/db/migrations/152_org_settings_cache_upload_settle_timeout.d.ts +15 -0
  22. package/dist/db/migrations/153_execution_runs_cancel_force.d.ts +16 -0
  23. package/dist/db/types.d.ts +15 -0
  24. package/dist/index.js +192 -53
  25. package/dist/pipeline/changed-files.d.ts +51 -0
  26. package/dist/pipeline/cross-source-tally.d.ts +21 -0
  27. package/dist/pipeline/degraded-reason.d.ts +8 -0
  28. package/dist/pipeline/dispatch-matched-workflow.d.ts +10 -0
  29. package/dist/pipeline/global-eval-round.d.ts +3 -2
  30. package/dist/pipeline/processor.d.ts +9 -1
  31. package/dist/pipeline/rerun-inputs.d.ts +16 -7
  32. package/dist/providers/github/changed-files.d.ts +14 -4
  33. package/dist/providers/universal-git/changed-files.d.ts +6 -0
  34. package/dist/providers/universal-git/config.d.ts +4 -0
  35. package/dist/scaler/container-backend.d.ts +2 -0
  36. package/dist/scaler/container-runtime.d.ts +9 -0
  37. package/dist/scaler/manager.d.ts +26 -2
  38. package/dist/server.js +2710 -1486
  39. package/dist/stale-detector/workflow-deadline-detector.d.ts +7 -1
  40. package/dist/standalone.js +11527 -10302
  41. package/dist/storage/s3.d.ts +36 -0
  42. package/dist/ws/agent-handler.d.ts +24 -3
  43. package/dist/ws/agent-heartbeat.d.ts +2 -2
  44. package/installer-image-digests.json +3 -3
  45. package/package.json +5 -5
  46. package/sbom.spdx.json +53 -53
@@ -258,6 +258,17 @@ export declare class Dispatcher {
258
258
  */
259
259
  hasPeerCoordinators?: () => boolean | Promise<boolean>;
260
260
  });
261
+ /** Runs with the ids of the jobs each disconnect triage failed; see {@link setDisconnectCleanup}. */
262
+ private disconnectCleanup?;
263
+ /**
264
+ * Install what runs with the ids of the jobs a disconnect triage failed. The
265
+ * agent WebSocket handler installs the release of their in-memory waiters
266
+ * (pending builds, inits, dynamic evaluations, global eval rounds). It runs
267
+ * inside {@link onAgentDisconnect}, so a path that drops an agent without
268
+ * waiting for its socket (heartbeat timeout, ack timeout, token revocation)
269
+ * reaches it too.
270
+ */
271
+ setDisconnectCleanup(cleanup: (failedJobIds: string[]) => void): void;
261
272
  /**
262
273
  * Dispatch a job to a matching agent, or queue it if none available.
263
274
  *
@@ -508,6 +519,15 @@ export declare class Dispatcher {
508
519
  * triage finds nothing to double-handle.
509
520
  */
510
521
  private handleAckExpiry;
522
+ /**
523
+ * Drop an agent that did not acknowledge a dispatch. Its other in-flight jobs
524
+ * (a multi-slot agent may run more than the one that timed out) go through
525
+ * {@link onAgentDisconnect}, which reads the registration before its first
526
+ * await to tell a scaler-managed agent from a static one, so the triage
527
+ * starts first. The unregister right after removes the agent from routing at
528
+ * once, so a redispatch cannot re-select it while the triage awaits.
529
+ */
530
+ private dropUnacknowledgingAgent;
511
531
  /**
512
532
  * Handle an explicit dispatch rejection (`job.reject`) from an agent.
513
533
  * Undoes the dispatch accounting and requeues the job for another agent.
@@ -634,6 +654,8 @@ export declare class Dispatcher {
634
654
  * Used by the cancel-run API. Returns null if the job is not tracked (e.g. already completed).
635
655
  */
636
656
  getAgentIdForJob(jobId: string): string | null;
657
+ /** The jobs of `runId` this dispatcher currently tracks to an agent. */
658
+ getTrackedJobIdsForRun(runId: string): string[];
637
659
  /**
638
660
  * Start periodic cleanup of expired grace window entries.
639
661
  * Should be called during server startup.
@@ -263,6 +263,37 @@ export declare class AgentRegistry {
263
263
  * registry behaves exactly as before.
264
264
  */
265
265
  constructor(deps?: AgentRegistryDeps);
266
+ /**
267
+ * Triages the in-flight jobs of an agent this registry drops on its own (a
268
+ * revoked or expired token): fails or requeues a scaler-managed agent's
269
+ * jobs, starts recovery for a static agent's. Installed by the dispatcher,
270
+ * which is built after the registry.
271
+ */
272
+ private disconnectTriage?;
273
+ /** Install the job triage {@link disconnectByTokenId} runs for each agent it drops. */
274
+ setDisconnectTriage(triage: (agentId: string) => Promise<unknown>): void;
275
+ /**
276
+ * Sockets whose `agent.register` is in flight (awaiting the scaler lookup),
277
+ * per agent id, with the id of the agent token each authenticated with
278
+ * (`null` without token auth). Kept here rather than in the WebSocket
279
+ * handler because the coordinator builds one handler per connection: a
280
+ * socket's close, and the check a registration makes against another agent
281
+ * token, must see a registration in flight on another connection.
282
+ */
283
+ private readonly registeringSockets;
284
+ /** Record that `ws`, authenticated with `tokenId`, has started registering `agentId`. */
285
+ beginRegistering(agentId: string, ws: WsLike, tokenId: string | null): void;
286
+ /** Record that `ws` has finished registering `agentId`, however it ended. */
287
+ endRegistering(agentId: string, ws: WsLike): void;
288
+ /** Whether a socket other than `ws` is registering `agentId` right now. */
289
+ isRegisteringElsewhere(agentId: string, ws: WsLike): boolean;
290
+ /**
291
+ * Whether another connection holds `agentId` under an agent token other than
292
+ * `tokenId`: the live registration, or a registration in flight on another
293
+ * socket. A registration without token auth (`null`) holds no token identity
294
+ * and conflicts with nothing.
295
+ */
296
+ isAgentIdHeldByOtherToken(agentId: string, tokenId: string, ws: WsLike): boolean;
266
297
  /**
267
298
  * Register an agent. If the agentId already exists, updates the existing
268
299
  * entry (agent reconnection scenario).
package/dist/app.d.ts CHANGED
@@ -41,6 +41,7 @@ import type { PendingInitTracker } from './cache/index.js';
41
41
  import type { PendingDynamicTracker, PendingGlobalEvalTracker } from './cache/index.js';
42
42
  import type { GlobalEvalRoundCache } from './cache/index.js';
43
43
  import type { CacheStorage } from './storage/types.js';
44
+ import type { CacheUploadSettle } from './cache/cache-upload-settle.js';
44
45
  import type { ProvenanceTrustRoot } from './provenance/trust-root.js';
45
46
  import type { LocalSigner } from './oidc/local-dev-signer.js';
46
47
  import type { CheckRunReporter } from './reporting/check-run-reporter.js';
@@ -178,6 +179,11 @@ export interface AppDependencies {
178
179
  dispatchCacheRefs?: DispatchCacheRefTracker;
179
180
  /** Cache storage backend (S3) for metadata operations on upload completion. */
180
181
  cacheStorage?: CacheStorage;
182
+ /**
183
+ * In-flight build-cache publishes plus the per-org settle bound. Constructed
184
+ * once by orchestrator-core: the agent-WS factory below runs per connection.
185
+ */
186
+ cacheUploadSettle?: CacheUploadSettle;
181
187
  /** Provenance trust root used to verify build-provenance bundles at ingest. */
182
188
  provenanceTrustRoot?: ProvenanceTrustRoot;
183
189
  /**
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Holding a build job's success until the cache publish its agent reported
3
+ * just before it has finished.
4
+ *
5
+ * A build agent sends `cache.upload.complete` and then, moments later, the
6
+ * build's terminal `job.status success`. The agent WebSocket handler runs
7
+ * frames concurrently, so without this record the success can release the
8
+ * jobs that wait on the build while the pointer naming the uploaded tarball is
9
+ * still being written — and they dispatch without the cache URL.
10
+ *
11
+ * The upload frame registers here before its first `await`. Frames enter the
12
+ * handler in wire order, so the registration is in place by the time the
13
+ * success frame is handled. The success then waits, bounded by the org's
14
+ * `cache_upload_settle_timeout_ms` (cluster default
15
+ * `KICI_CACHE_UPLOAD_SETTLE_TIMEOUT_MS`).
16
+ */
17
+ import type { Kysely } from 'kysely';
18
+ import type { Database } from '../db/types.js';
19
+ /** How a build success's wait for its cache publish ended. */
20
+ export declare enum CacheUploadSettleOutcome {
21
+ /** No publish was in flight for the job. */
22
+ None = "none",
23
+ /** The effective timeout is 0: the wait is turned off. */
24
+ Disabled = "disabled",
25
+ /** Every in-flight publish finished, successfully or not. */
26
+ Settled = "settled",
27
+ /** The bound passed first. The publish may still finish afterwards. */
28
+ TimedOut = "timed-out"
29
+ }
30
+ /**
31
+ * The cache publishes in flight, per job. A full build registers two (the
32
+ * dependency tarball and the source tarball); a publish always settles —
33
+ * success or failure — so a waiter never sees a rejection.
34
+ */
35
+ export declare class InFlightCacheUploads {
36
+ private readonly inFlight;
37
+ /** Register one publish for `jobId`; the returned function settles it (idempotent). */
38
+ begin(jobId: string): () => void;
39
+ has(jobId: string): boolean;
40
+ /** Number of jobs with a publish in flight. */
41
+ get size(): number;
42
+ /**
43
+ * Wait for every publish registered for `jobId` at call time, bounded by
44
+ * `timeoutMs`. A timeout leaves the records in place: they clear when their
45
+ * publishes settle.
46
+ */
47
+ waitFor(jobId: string, timeoutMs: number): Promise<CacheUploadSettleOutcome>;
48
+ }
49
+ /** What the agent WebSocket handler needs to hold a build's success. */
50
+ export interface CacheUploadSettle {
51
+ uploads: InFlightCacheUploads;
52
+ /** The effective bound for the job's org (org override, else cluster default). */
53
+ timeoutMsFor: (orgId: string | undefined) => Promise<number>;
54
+ }
55
+ export interface CacheUploadSettleResult {
56
+ outcome: CacheUploadSettleOutcome;
57
+ timeoutMs?: number;
58
+ waitedMs?: number;
59
+ }
60
+ /**
61
+ * Wait for a job's in-flight cache publishes. Reads the setting only when a
62
+ * publish is in flight, so an ordinary job's success costs no query.
63
+ */
64
+ export declare function awaitCacheUploadSettle(settle: CacheUploadSettle | undefined, job: {
65
+ jobId: string;
66
+ orgId: string | undefined;
67
+ }): Promise<CacheUploadSettleResult>;
68
+ /**
69
+ * Resolve the settle bound for an org: `org_settings.cache_upload_settle_timeout_ms`
70
+ * when set (0 included), otherwise `fallbackMs`. A missing DB, org, row or
71
+ * value, or a failed query, falls back — the caller is a frame handler that
72
+ * must not reject.
73
+ */
74
+ export declare function createCacheUploadSettleTimeoutReader(db: Kysely<Database> | undefined, fallbackMs: number): (orgId: string | undefined) => Promise<number>;
75
+ //# sourceMappingURL=cache-upload-settle.d.ts.map
@@ -28,6 +28,15 @@ export interface InitResult {
28
28
  * explicit `false` suppresses.
29
29
  */
30
30
  filterPassed?: boolean;
31
+ /**
32
+ * Verdict on the workflow's deferred `paths`, reported only when the init job
33
+ * was asked to decide them. `false` means the agent's clone diff matched none
34
+ * of them and the job must not be dispatched.
35
+ *
36
+ * Optional on purpose: an agent that predates deferred paths never sends it,
37
+ * so absence means "no verdict" and dispatch proceeds.
38
+ */
39
+ pathsPassed?: boolean;
31
40
  }
32
41
  export declare class PendingInitTracker extends PendingTracker<InitResult> {
33
42
  constructor();
@@ -33,11 +33,12 @@ export interface CancelRunDeps {
33
33
  */
34
34
  instanceId?: string;
35
35
  /**
36
- * Forward `job.cancel` to the coordinator holding the agent's socket.
37
- * Returns whether the send left this process. Undefined ⇒ no peer transport,
38
- * so a job owned by a sibling is unreachable rather than orphaned.
36
+ * Forward `job.cancel` to the coordinator holding the agent's socket, with
37
+ * `force` when the cancel is forced. Returns whether the send left this
38
+ * process. Undefined ⇒ no peer transport, so a job owned by a sibling is
39
+ * unreachable rather than orphaned.
39
40
  */
40
- cancelJobOnPeer?: (peerId: string, runId: string, jobId: string, reason: string) => boolean;
41
+ cancelJobOnPeer?: (peerId: string, runId: string, jobId: string, reason: string, force?: boolean) => boolean;
41
42
  /** How stale a `cluster_instances` heartbeat may be and still read as live. */
42
43
  ownershipGraceMs?: number;
43
44
  /**
@@ -112,4 +113,21 @@ export declare class HeldRunCancelRefusedError extends Error {
112
113
  * returns a structured already-terminal result).
113
114
  */
114
115
  export declare function cancelRunWithReason(deps: CancelRunDeps, runId: string, reason: string, options?: CancelRunOptions): Promise<CancelRunResult>;
116
+ /**
117
+ * Send `job.cancel` down this coordinator's own socket for the agent.
118
+ *
119
+ * Only an OPEN socket can receive it. A socket mid-close is still in the
120
+ * registry until its close handler unregisters it, and sending to it throws, so
121
+ * a non-OPEN socket leaves the job un-notified and the orphan sweep takes it.
122
+ * The send is wrapped so one bad socket cannot abort the whole cancellation and
123
+ * strand the run in `cancelling`.
124
+ */
125
+ export declare function sendLocalCancel(args: {
126
+ registry: Pick<AgentRegistry, 'get'>;
127
+ runId: string;
128
+ jobId: string;
129
+ agentId: string | null;
130
+ reason: string;
131
+ force: boolean;
132
+ }): boolean;
115
133
  //# sourceMappingURL=cancel-run.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The receiving end of `peer.job.cancel`.
3
+ *
4
+ * A sibling forwards a cancel to the coordinator its agent is connected to,
5
+ * which it reads from the host roster. That coordinator's dispatcher tracks the
6
+ * job only when it dispatched or re-adopted it: an agent that reconnected here
7
+ * from a sibling, or one not yet back after this process restarted, is known
8
+ * only to `dispatch_queue.agent_id`. So the agent is resolved from the
9
+ * dispatcher first and from the dispatch record second, and a cancel that
10
+ * cannot be delivered is logged with the reason.
11
+ */
12
+ import type { Kysely } from 'kysely';
13
+ import { z } from 'zod';
14
+ import type { PeerJobCancel } from '@kici-dev/engine';
15
+ import type { Database } from '../db/types.js';
16
+ import type { AgentRegistry } from '../agent/registry.js';
17
+ import type { Dispatcher } from '../agent/dispatcher.js';
18
+ export declare const PeerCancelOutcome: z.ZodEnum<{
19
+ "agent-not-connected": "agent-not-connected";
20
+ delivered: "delivered";
21
+ "lookup-failed": "lookup-failed";
22
+ "not-tracked": "not-tracked";
23
+ }>;
24
+ export type PeerCancelOutcome = z.infer<typeof PeerCancelOutcome>;
25
+ /** A live dispatch row: the job and the agent it was handed to. */
26
+ export interface DispatchedAgent {
27
+ jobId: string;
28
+ agentId: string | null;
29
+ }
30
+ export interface PeerCancelDelivery extends DispatchedAgent {
31
+ outcome: PeerCancelOutcome;
32
+ }
33
+ export interface PeerJobCancelDeps {
34
+ dispatcher: Pick<Dispatcher, 'getAgentIdForJob' | 'getTrackedJobIdsForRun'>;
35
+ registry: Pick<AgentRegistry, 'get'>;
36
+ /** The durable dispatch record. Absent on a worker, which has no database. */
37
+ lookupDispatched?: (runId: string, jobId: string | undefined) => Promise<DispatchedAgent[]>;
38
+ logger: {
39
+ info(message: string, meta?: object): unknown;
40
+ warn(message: string, meta?: object): unknown;
41
+ };
42
+ }
43
+ /** Read the live dispatch rows of a run, or of one job of it. */
44
+ export declare function readDispatchedAgents(db: Kysely<Database>, runId: string, jobId: string | undefined): Promise<DispatchedAgent[]>;
45
+ /**
46
+ * Deliver a `peer.job.cancel` to the agents connected here. Resolves; never
47
+ * rejects: every failure is an outcome and a warn line.
48
+ */
49
+ export declare function deliverPeerJobCancel(deps: PeerJobCancelDeps, msg: PeerJobCancel): Promise<PeerCancelDelivery[]>;
50
+ //# sourceMappingURL=peer-job-cancel.d.ts.map
@@ -3,7 +3,9 @@ import type { Database } from '../db/types.js';
3
3
  export interface SweepStuckCancellingDeps {
4
4
  db: Kysely<Database>;
5
5
  /** The canonical cancel path, bound to this orchestrator's deps. */
6
- cancelRun: (runId: string, reason: string) => Promise<unknown>;
6
+ cancelRun: (runId: string, reason: string, options: {
7
+ force: boolean;
8
+ }) => Promise<unknown>;
7
9
  /**
8
10
  * How long a run may sit in `cancelling` before it is re-driven. Twice the
9
11
  * recovery grace period: long enough that a graceful agent teardown finishes
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Render the post-receive hook script. For each pushed ref the hook calls
3
- * `kici-admin source trigger-local <id> --ref <ref> --sha <new> --base-url <url>`,
4
- * so a push to any branch dispatches a run for that ref.
3
+ * `kici-admin source trigger-local <id> --ref <ref> --before <old> --sha <new> --base-url <url>`,
4
+ * so a push to any branch dispatches a run for that ref, and its changed files
5
+ * are the commits the push added (`<old>..<new>`).
5
6
  */
6
7
  export declare function renderPostReceiveHook(opts: {
7
8
  sourceId: string;
@@ -5,6 +5,11 @@ export interface LocalTriggerInput {
5
5
  event: 'push' | 'pull_request';
6
6
  ref: string;
7
7
  sha: string;
8
+ /**
9
+ * The commit before the push. With it the changed files are `before..sha`;
10
+ * without it the push has no range and path filters match conservatively.
11
+ */
12
+ before?: string;
8
13
  defaultBranch: string;
9
14
  }
10
15
  export interface LocalTriggerRequest {
@@ -19,6 +24,8 @@ export declare function readRepoHead(repoPath: string): {
19
24
  ref: string;
20
25
  sha: string;
21
26
  };
27
+ /** The parent of `sha` in a local repo, or undefined for a root commit or an unreadable repo. */
28
+ export declare function readParentSha(repoPath: string, sha: string): string | undefined;
22
29
  /** POST the trigger request to the orchestrator base URL. Returns the HTTP status. */
23
30
  export declare function sendLocalTrigger(baseUrl: string, req: LocalTriggerRequest): Promise<number>;
24
31
  //# sourceMappingURL=local-trigger.d.ts.map