@kici-dev/orchestrator 0.1.17 → 0.1.19

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 (42) hide show
  1. package/dist/agent/dispatcher.d.ts +9 -0
  2. package/dist/agent/host-roster-reaper.d.ts +42 -0
  3. package/dist/agent/host-roster.d.ts +110 -0
  4. package/dist/agent/registry.d.ts +65 -2
  5. package/dist/app.d.ts +3 -0
  6. package/dist/cli/commands/host.d.ts +13 -0
  7. package/dist/cli/commands/shared/versioned-upgrade.d.ts +23 -0
  8. package/dist/cli/service/compose.d.ts +2 -1
  9. package/dist/cli/service/index.d.ts +2 -2
  10. package/dist/cli/service/instance/manifest.d.ts +12 -0
  11. package/dist/cli/service/instance/resolve.d.ts +12 -2
  12. package/dist/cli/service/launchd.d.ts +4 -1
  13. package/dist/cli/service/platform-detect.d.ts +5 -0
  14. package/dist/cli/service/systemd.d.ts +2 -1
  15. package/dist/cli/service/types.d.ts +37 -0
  16. package/dist/cli/service/windows.d.ts +2 -1
  17. package/dist/cli.js +797 -202
  18. package/dist/cluster/coordinator.d.ts +5 -1
  19. package/dist/config/schema.d.ts +4 -0
  20. package/dist/config/types.d.ts +9 -0
  21. package/dist/config.d.ts +6 -0
  22. package/dist/db/migrations/039_host_roster.d.ts +19 -0
  23. package/dist/db/migrations/040_runsonall_pin.d.ts +4 -0
  24. package/dist/db/migrations/041_wave_gated.d.ts +4 -0
  25. package/dist/db/migrations/042_dispatch_queue_patterns.d.ts +4 -0
  26. package/dist/db/types.d.ts +61 -0
  27. package/dist/diagnostics/fleet-collector.d.ts +1 -1
  28. package/dist/environments/held-runs.d.ts +9 -0
  29. package/dist/lockfile-redos-guard.d.ts +19 -0
  30. package/dist/metrics/prometheus.d.ts +13 -0
  31. package/dist/metrics/scheduled-jobs.d.ts +2 -2
  32. package/dist/orchestrator-core.d.ts +46 -1
  33. package/dist/pipeline/dispatch-matched-workflow.d.ts +42 -1
  34. package/dist/pipeline/processor.d.ts +25 -0
  35. package/dist/pipeline/wave-scheduler.d.ts +60 -0
  36. package/dist/queue/job-queue.d.ts +61 -2
  37. package/dist/reporting/execution-tracker.d.ts +28 -0
  38. package/dist/server.js +52760 -51263
  39. package/dist/standalone.js +1953 -500
  40. package/installer-image-digests.json +3 -3
  41. package/package.json +22 -22
  42. package/sbom.spdx.json +3227 -6682
package/dist/cli.js CHANGED
@@ -19,14 +19,14 @@ import { Migrator } from "kysely/migration";
19
19
  import "jsonpath-plus";
20
20
  import { execFile, execFileSync, execSync, spawn } from "node:child_process";
21
21
  import { promisify } from "node:util";
22
- import archiver from "archiver";
22
+ import { ZipArchive } from "archiver";
23
23
  import { confirm, input, password, select } from "@inquirer/prompts";
24
24
  import JSZip from "jszip";
25
25
  import { pipeline } from "node:stream/promises";
26
26
  import "pg";
27
+ import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
27
28
  import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
28
29
  import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
29
- import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, minAccessLogWarmDays, minSecretAuditLogWarmDays, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
30
30
  import http from "node:http";
31
31
  import { PassThrough } from "node:stream";
32
32
  import Docker from "dockerode";
@@ -979,6 +979,8 @@ var init_schema = __esmMin((() => {
979
979
  }).optional(),
980
980
  agentAuth: z.enum(["token", "none"]).optional(),
981
981
  agentTokenTtlMs: z.coerce.number().optional(),
982
+ rosterGraceMs: z.coerce.number().optional(),
983
+ rosterTtlMs: z.coerce.number().optional(),
982
984
  queue: z.object({
983
985
  maxDepth: z.coerce.number().optional(),
984
986
  timeoutMs: z.coerce.number().optional(),
@@ -1055,6 +1057,8 @@ var init_schema = __esmMin((() => {
1055
1057
  }).optional(),
1056
1058
  agentAuth: z.enum(["token", "none"]).default("token"),
1057
1059
  agentTokenTtlMs: z.coerce.number().default(36e5),
1060
+ rosterGraceMs: z.coerce.number().default(3e5),
1061
+ rosterTtlMs: z.coerce.number().default(18e5),
1058
1062
  queueMaxDepth: z.coerce.number().default(1e3),
1059
1063
  queueTimeoutMs: z.coerce.number().default(36e5),
1060
1064
  /**
@@ -1550,8 +1554,8 @@ var init_client = __esmMin((() => {
1550
1554
  //#region src/db/migrations/001_initial.ts
1551
1555
  init_client();
1552
1556
  var _001_initial_exports = /* @__PURE__ */ __exportAll({
1553
- down: () => down$37,
1554
- up: () => up$37
1557
+ down: () => down$41,
1558
+ up: () => up$41
1555
1559
  });
1556
1560
  /**
1557
1561
  * Squashed initial migration -- creates the complete Orchestrator database schema.
@@ -2243,7 +2247,7 @@ const DDL_STATEMENTS = [
2243
2247
  `ALTER TABLE ONLY public.held_runs
2244
2248
  ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
2245
2249
  ];
2246
- async function up$37(db) {
2250
+ async function up$41(db) {
2247
2251
  for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
2248
2252
  await sql`
2249
2253
  INSERT INTO cluster_meta (key, value)
@@ -2265,7 +2269,7 @@ async function up$37(db) {
2265
2269
  * Rollback drops everything created above. Uses CASCADE on table drops to cut
2266
2270
  * through the FK graph without relying on exact topological order.
2267
2271
  */
2268
- async function down$37(db) {
2272
+ async function down$41(db) {
2269
2273
  for (const [trig, tbl] of [["source_secrets_change_trigger", "scoped_secrets"], ["sources_change_trigger", "sources"]]) await sql.raw(`DROP TRIGGER IF EXISTS ${trig} ON public.${tbl}`).execute(db);
2270
2274
  for (const table of [
2271
2275
  "workflow_registrations",
@@ -2312,8 +2316,8 @@ async function down$37(db) {
2312
2316
  //#endregion
2313
2317
  //#region src/db/migrations/002_config_versions_key_version.ts
2314
2318
  var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2315
- down: () => down$36,
2316
- up: () => up$36
2319
+ down: () => down$40,
2320
+ up: () => up$40
2317
2321
  });
2318
2322
  /**
2319
2323
  * Add key_version column to config_versions so that sensitive-field encryption
@@ -2327,13 +2331,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2327
2331
  * No index is needed: rotation does a full-table scan; reads are by
2328
2332
  * `version` primary key and never filter on `key_version`.
2329
2333
  */
2330
- async function up$36(db) {
2334
+ async function up$40(db) {
2331
2335
  await sql`
2332
2336
  ALTER TABLE public.config_versions
2333
2337
  ADD COLUMN key_version integer NOT NULL DEFAULT 1
2334
2338
  `.execute(db);
2335
2339
  }
2336
- async function down$36(db) {
2340
+ async function down$40(db) {
2337
2341
  await sql`
2338
2342
  ALTER TABLE public.config_versions
2339
2343
  DROP COLUMN key_version
@@ -2342,8 +2346,8 @@ async function down$36(db) {
2342
2346
  //#endregion
2343
2347
  //#region src/db/migrations/003_access_log.ts
2344
2348
  var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2345
- down: () => down$35,
2346
- up: () => up$35
2349
+ down: () => down$39,
2350
+ up: () => up$39
2347
2351
  });
2348
2352
  /**
2349
2353
  * Access log: one row per read or orchestrator-admin mutation attributable
@@ -2367,7 +2371,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2367
2371
  * Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
2368
2372
  * cleanup.ts picks up the prune pass.
2369
2373
  */
2370
- async function up$35(db) {
2374
+ async function up$39(db) {
2371
2375
  await sql`
2372
2376
  CREATE TABLE public.access_log (
2373
2377
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -2405,28 +2409,28 @@ async function up$35(db) {
2405
2409
  ON public.access_log (actor_type, actor_id, created_at DESC)
2406
2410
  `.execute(db);
2407
2411
  }
2408
- async function down$35(db) {
2412
+ async function down$39(db) {
2409
2413
  await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
2410
2414
  }
2411
2415
  //#endregion
2412
2416
  //#region src/db/migrations/004_rename_bundle_to_source.ts
2413
2417
  var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
2414
- down: () => down$34,
2415
- up: () => up$34
2418
+ down: () => down$38,
2419
+ up: () => up$38
2416
2420
  });
2417
- async function up$34(db) {
2421
+ async function up$38(db) {
2418
2422
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
2419
2423
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
2420
2424
  }
2421
- async function down$34(db) {
2425
+ async function down$38(db) {
2422
2426
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
2423
2427
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
2424
2428
  }
2425
2429
  //#endregion
2426
2430
  //#region src/db/migrations/005_cold_store_chunk_counter.ts
2427
2431
  var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2428
- down: () => down$33,
2429
- up: () => up$33
2432
+ down: () => down$37,
2433
+ up: () => up$37
2430
2434
  });
2431
2435
  /**
2432
2436
  * Cold-store chunk counter table.
@@ -2443,7 +2447,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2443
2447
  *
2444
2448
  * sections 5 and 8.
2445
2449
  */
2446
- async function up$33(db) {
2450
+ async function up$37(db) {
2447
2451
  await sql`
2448
2452
  CREATE TABLE public.cold_store_chunk_counts (
2449
2453
  db TEXT NOT NULL,
@@ -2461,14 +2465,14 @@ async function up$33(db) {
2461
2465
  ON public.cold_store_chunk_counts (db, table_name)
2462
2466
  `.execute(db);
2463
2467
  }
2464
- async function down$33(db) {
2468
+ async function down$37(db) {
2465
2469
  await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
2466
2470
  }
2467
2471
  //#endregion
2468
2472
  //#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
2469
2473
  var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2470
- down: () => down$32,
2471
- up: () => up$32
2474
+ down: () => down$36,
2475
+ up: () => up$36
2472
2476
  });
2473
2477
  /**
2474
2478
  * `execution_runs` / `execution_jobs` / `execution_steps` cold-store
@@ -2504,7 +2508,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2504
2508
  * - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
2505
2509
  * - `idx_execution_steps_routing_key_created (routing_key, created_at)`
2506
2510
  */
2507
- async function up$32(db) {
2511
+ async function up$36(db) {
2508
2512
  await sql`
2509
2513
  ALTER TABLE public.execution_runs
2510
2514
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2549,7 +2553,7 @@ async function up$32(db) {
2549
2553
  ON public.execution_steps (routing_key, created_at)
2550
2554
  `.execute(db);
2551
2555
  }
2552
- async function down$32(db) {
2556
+ async function down$36(db) {
2553
2557
  await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
2554
2558
  await sql`
2555
2559
  ALTER TABLE public.execution_steps
@@ -2574,8 +2578,8 @@ async function down$32(db) {
2574
2578
  //#endregion
2575
2579
  //#region src/db/migrations/007_audit_logs_archived_at.ts
2576
2580
  var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2577
- down: () => down$31,
2578
- up: () => up$31
2581
+ down: () => down$35,
2582
+ up: () => up$35
2579
2583
  });
2580
2584
  /**
2581
2585
  * `secret_audit_log` and `access_log` cold-store schema additions, plus
@@ -2615,7 +2619,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2615
2619
  * `down()` would not have meaningful retention bounds. Acceptable for
2616
2620
  * staging.
2617
2621
  */
2618
- async function up$31(db) {
2622
+ async function up$35(db) {
2619
2623
  await sql`
2620
2624
  ALTER TABLE public.secret_audit_log
2621
2625
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2636,7 +2640,7 @@ async function up$31(db) {
2636
2640
  DROP COLUMN IF EXISTS expires_at
2637
2641
  `.execute(db);
2638
2642
  }
2639
- async function down$31(db) {
2643
+ async function down$35(db) {
2640
2644
  await sql`
2641
2645
  ALTER TABLE public.access_log
2642
2646
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
@@ -2664,8 +2668,8 @@ async function down$31(db) {
2664
2668
  //#endregion
2665
2669
  //#region src/db/migrations/008_event_log_archived_at.ts
2666
2670
  var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2667
- down: () => down$30,
2668
- up: () => up$30
2671
+ down: () => down$34,
2672
+ up: () => up$34
2669
2673
  });
2670
2674
  /**
2671
2675
  * `event_log` cold-store schema additions plus removal of the
@@ -2703,7 +2707,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2703
2707
  * — best effort; rows inserted between `up()` and a hypothetical
2704
2708
  * `down()` would not have meaningful retention bounds.
2705
2709
  */
2706
- async function up$30(db) {
2710
+ async function up$34(db) {
2707
2711
  await sql`
2708
2712
  ALTER TABLE public.event_log
2709
2713
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2719,7 +2723,7 @@ async function up$30(db) {
2719
2723
  DROP COLUMN IF EXISTS expires_at
2720
2724
  `.execute(db);
2721
2725
  }
2722
- async function down$30(db) {
2726
+ async function down$34(db) {
2723
2727
  await sql`
2724
2728
  ALTER TABLE public.event_log
2725
2729
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
@@ -2738,8 +2742,8 @@ async function down$30(db) {
2738
2742
  //#endregion
2739
2743
  //#region src/db/migrations/009_access_log_trigram.ts
2740
2744
  var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2741
- down: () => down$29,
2742
- up: () => up$29
2745
+ down: () => down$33,
2746
+ up: () => up$33
2743
2747
  });
2744
2748
  /**
2745
2749
  * Trigram (pg_trgm) index on access_log.error_message for the federated
@@ -2752,7 +2756,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2752
2756
  * EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
2753
2757
  * migrations inside a transaction; the lock is brief on a sampled table.
2754
2758
  */
2755
- async function up$29(db) {
2759
+ async function up$33(db) {
2756
2760
  await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
2757
2761
  await sql`
2758
2762
  CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
@@ -2761,14 +2765,14 @@ async function up$29(db) {
2761
2765
  WHERE error_message IS NOT NULL
2762
2766
  `.execute(db);
2763
2767
  }
2764
- async function down$29(db) {
2768
+ async function down$33(db) {
2765
2769
  await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
2766
2770
  }
2767
2771
  //#endregion
2768
2772
  //#region src/db/migrations/010_cold_store_chunks.ts
2769
2773
  var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2770
- down: () => down$28,
2771
- up: () => up$28
2774
+ down: () => down$32,
2775
+ up: () => up$32
2772
2776
  });
2773
2777
  /**
2774
2778
  * Cold-store chunk index — Phase 2 (cold-store purge).
@@ -2800,7 +2804,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2800
2804
  * forever. Adapters that don't opt into per-bucket archival via
2801
2805
  * `coldTtlDays` don't insert here either.
2802
2806
  */
2803
- async function up$28(db) {
2807
+ async function up$32(db) {
2804
2808
  await sql`
2805
2809
  CREATE TABLE public.cold_store_chunks (
2806
2810
  db TEXT NOT NULL,
@@ -2827,14 +2831,14 @@ async function up$28(db) {
2827
2831
  ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
2828
2832
  `.execute(db);
2829
2833
  }
2830
- async function down$28(db) {
2834
+ async function down$32(db) {
2831
2835
  await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
2832
2836
  }
2833
2837
  //#endregion
2834
2838
  //#region src/db/migrations/011_drop_source_secrets_notify.ts
2835
2839
  var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2836
- down: () => down$27,
2837
- up: () => up$27
2840
+ down: () => down$31,
2841
+ up: () => up$31
2838
2842
  });
2839
2843
  /**
2840
2844
  * Drop the `source_secrets_change_trigger` and the
@@ -2853,11 +2857,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2853
2857
  * in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
2854
2858
  * is restored, so this migration is safe to roll back.
2855
2859
  */
2856
- async function up$27(db) {
2860
+ async function up$31(db) {
2857
2861
  await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
2858
2862
  await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
2859
2863
  }
2860
- async function down$27(db) {
2864
+ async function down$31(db) {
2861
2865
  await sql`
2862
2866
  CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
2863
2867
  LANGUAGE plpgsql
@@ -2896,8 +2900,8 @@ async function down$27(db) {
2896
2900
  //#endregion
2897
2901
  //#region src/db/migrations/012_peer_credentials_active_uniq.ts
2898
2902
  var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2899
- down: () => down$26,
2900
- up: () => up$26
2903
+ down: () => down$30,
2904
+ up: () => up$30
2901
2905
  });
2902
2906
  /**
2903
2907
  * Add a partial unique index on `peer_credentials (instance_id) WHERE
@@ -2923,7 +2927,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2923
2927
  * `down()` only drops the index; it does NOT undo the dedupe (there's no
2924
2928
  * safe way to recreate revoked rows, and the dedupe is monotonic).
2925
2929
  */
2926
- async function up$26(db) {
2930
+ async function up$30(db) {
2927
2931
  await sql`
2928
2932
  UPDATE public.peer_credentials
2929
2933
  SET revoked_at = NOW()
@@ -2941,14 +2945,14 @@ async function up$26(db) {
2941
2945
  WHERE revoked_at IS NULL
2942
2946
  `.execute(db);
2943
2947
  }
2944
- async function down$26(db) {
2948
+ async function down$30(db) {
2945
2949
  await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
2946
2950
  }
2947
2951
  //#endregion
2948
2952
  //#region src/db/migrations/013_execution_log_bytes.ts
2949
2953
  var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2950
- down: () => down$25,
2951
- up: () => up$25
2954
+ down: () => down$29,
2955
+ up: () => up$29
2952
2956
  });
2953
2957
  /**
2954
2958
  * Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
@@ -2967,7 +2971,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2967
2971
  *
2968
2972
  * Idempotent (`ADD COLUMN IF NOT EXISTS`).
2969
2973
  */
2970
- async function up$25(db) {
2974
+ async function up$29(db) {
2971
2975
  await sql`
2972
2976
  ALTER TABLE public.execution_runs
2973
2977
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
@@ -2977,15 +2981,15 @@ async function up$25(db) {
2977
2981
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
2978
2982
  `.execute(db);
2979
2983
  }
2980
- async function down$25(db) {
2984
+ async function down$29(db) {
2981
2985
  await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2982
2986
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2983
2987
  }
2984
2988
  //#endregion
2985
2989
  //#region src/db/migrations/014_kici_events_lease_retry.ts
2986
2990
  var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2987
- down: () => down$24,
2988
- up: () => up$24
2991
+ down: () => down$28,
2992
+ up: () => up$28
2989
2993
  });
2990
2994
  /**
2991
2995
  * Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
@@ -3019,7 +3023,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
3019
3023
  *
3020
3024
  * Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
3021
3025
  */
3022
- async function up$24(db) {
3026
+ async function up$28(db) {
3023
3027
  await sql`
3024
3028
  ALTER TABLE public.kici_events
3025
3029
  ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
@@ -3050,7 +3054,7 @@ async function up$24(db) {
3050
3054
  WHERE dlq_at IS NOT NULL
3051
3055
  `.execute(db);
3052
3056
  }
3053
- async function down$24(db) {
3057
+ async function down$28(db) {
3054
3058
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
3055
3059
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
3056
3060
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
@@ -3068,8 +3072,8 @@ async function down$24(db) {
3068
3072
  //#endregion
3069
3073
  //#region src/db/migrations/015_org_settings_customer_scoped.ts
3070
3074
  var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3071
- down: () => down$23,
3072
- up: () => up$23
3075
+ down: () => down$27,
3076
+ up: () => up$27
3073
3077
  });
3074
3078
  /**
3075
3079
  * Org-scope `org_settings` and qualify each glob entry by source.
@@ -3097,7 +3101,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3097
3101
  * Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
3098
3102
  * and the list columns are already jsonb, so it is a no-op.
3099
3103
  */
3100
- async function up$23(db) {
3104
+ async function up$27(db) {
3101
3105
  if ((await sql`
3102
3106
  SELECT EXISTS (
3103
3107
  SELECT 1 FROM information_schema.columns
@@ -3222,7 +3226,7 @@ async function up$23(db) {
3222
3226
  await sql`DROP TABLE _org_settings_merged`.execute(db);
3223
3227
  await sql`DROP TABLE _org_settings_stage`.execute(db);
3224
3228
  }
3225
- async function down$23(db) {
3229
+ async function down$27(db) {
3226
3230
  if (!(await sql`
3227
3231
  SELECT EXISTS (
3228
3232
  SELECT 1 FROM information_schema.columns
@@ -3247,8 +3251,8 @@ async function down$23(db) {
3247
3251
  //#endregion
3248
3252
  //#region src/db/migrations/016_org_settings_allow_http_npm.ts
3249
3253
  var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3250
- down: () => down$22,
3251
- up: () => up$22
3254
+ down: () => down$26,
3255
+ up: () => up$26
3252
3256
  });
3253
3257
  /**
3254
3258
  * Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
@@ -3261,7 +3265,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3261
3265
  *
3262
3266
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3263
3267
  */
3264
- async function up$22(db) {
3268
+ async function up$26(db) {
3265
3269
  if ((await sql`
3266
3270
  SELECT EXISTS (
3267
3271
  SELECT 1 FROM information_schema.columns
@@ -3275,7 +3279,7 @@ async function up$22(db) {
3275
3279
  ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
3276
3280
  `.execute(db);
3277
3281
  }
3278
- async function down$22(db) {
3282
+ async function down$26(db) {
3279
3283
  await sql`
3280
3284
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
3281
3285
  `.execute(db);
@@ -3283,8 +3287,8 @@ async function down$22(db) {
3283
3287
  //#endregion
3284
3288
  //#region src/db/migrations/017_org_id_widen.ts
3285
3289
  var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
3286
- down: () => down$21,
3287
- up: () => up$21
3290
+ down: () => down$25,
3291
+ up: () => up$25
3288
3292
  });
3289
3293
  /**
3290
3294
  * Widen every orchestrator-side `org_id varchar(12)` column to
@@ -3324,17 +3328,17 @@ const ORG_ID_TABLES$1 = [
3324
3328
  "held_runs",
3325
3329
  "scoped_secrets"
3326
3330
  ];
3327
- async function up$21(db) {
3331
+ async function up$25(db) {
3328
3332
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
3329
3333
  }
3330
- async function down$21(db) {
3334
+ async function down$25(db) {
3331
3335
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
3332
3336
  }
3333
3337
  //#endregion
3334
3338
  //#region src/db/migrations/018_org_id_prefix_backfill.ts
3335
3339
  var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
3336
- down: () => down$20,
3337
- up: () => up$20
3340
+ down: () => down$24,
3341
+ up: () => up$24
3338
3342
  });
3339
3343
  /**
3340
3344
  * Prefix every orchestrator-side tenant string with `org_` to align
@@ -3379,19 +3383,19 @@ const CUSTOMER_ID_TABLES = [
3379
3383
  "workflow_registrations",
3380
3384
  "org_settings"
3381
3385
  ];
3382
- async function up$20(db) {
3386
+ async function up$24(db) {
3383
3387
  for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = 'org_' || org_id WHERE org_id <> 'kici-admin' AND org_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3384
3388
  for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = 'org_' || customer_id WHERE customer_id <> 'kici-admin' AND customer_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3385
3389
  }
3386
- async function down$20(db) {
3390
+ async function down$24(db) {
3387
3391
  for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = substring(customer_id from 5) WHERE customer_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3388
3392
  for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = substring(org_id from 5) WHERE org_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3389
3393
  }
3390
3394
  //#endregion
3391
3395
  //#region src/db/migrations/019_generic_sources_change_notify.ts
3392
3396
  var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3393
- down: () => down$19,
3394
- up: () => up$19
3397
+ down: () => down$23,
3398
+ up: () => up$23
3395
3399
  });
3396
3400
  /**
3397
3401
  * Add a Postgres trigger on `generic_webhook_sources` that emits
@@ -3414,7 +3418,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3414
3418
  * (`notify_sources_change()` + `sources_change_trigger`, defined in
3415
3419
  * `001_initial.ts`).
3416
3420
  */
3417
- async function up$19(db) {
3421
+ async function up$23(db) {
3418
3422
  await sql`
3419
3423
  CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
3420
3424
  LANGUAGE plpgsql
@@ -3435,15 +3439,15 @@ async function up$19(db) {
3435
3439
  FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
3436
3440
  `.execute(db);
3437
3441
  }
3438
- async function down$19(db) {
3442
+ async function down$23(db) {
3439
3443
  await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
3440
3444
  await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
3441
3445
  }
3442
3446
  //#endregion
3443
3447
  //#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
3444
3448
  var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
3445
- down: () => down$18,
3446
- up: () => up$18
3449
+ down: () => down$22,
3450
+ up: () => up$22
3447
3451
  });
3448
3452
  /**
3449
3453
  * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
@@ -3458,7 +3462,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
3458
3462
  *
3459
3463
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3460
3464
  */
3461
- async function up$18(db) {
3465
+ async function up$22(db) {
3462
3466
  if ((await sql`
3463
3467
  SELECT EXISTS (
3464
3468
  SELECT 1 FROM information_schema.columns
@@ -3472,7 +3476,7 @@ async function up$18(db) {
3472
3476
  ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
3473
3477
  `.execute(db);
3474
3478
  }
3475
- async function down$18(db) {
3479
+ async function down$22(db) {
3476
3480
  await sql`
3477
3481
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
3478
3482
  `.execute(db);
@@ -3480,8 +3484,8 @@ async function down$18(db) {
3480
3484
  //#endregion
3481
3485
  //#region src/db/migrations/021_check_run_tracking.ts
3482
3486
  var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3483
- down: () => down$17,
3484
- up: () => up$17
3487
+ down: () => down$21,
3488
+ up: () => up$21
3485
3489
  });
3486
3490
  /**
3487
3491
  * Add `check_run_tracking` table for HA-safe check-run state persistence.
@@ -3505,7 +3509,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3505
3509
  *
3506
3510
  * Idempotent: a re-run on a DB that already has the table is a no-op.
3507
3511
  */
3508
- async function up$17(db) {
3512
+ async function up$21(db) {
3509
3513
  if ((await sql`
3510
3514
  SELECT EXISTS (
3511
3515
  SELECT 1 FROM information_schema.tables
@@ -3536,14 +3540,14 @@ async function up$17(db) {
3536
3540
  WHERE run_id IS NOT NULL
3537
3541
  `.execute(db);
3538
3542
  }
3539
- async function down$17(db) {
3543
+ async function down$21(db) {
3540
3544
  await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
3541
3545
  }
3542
3546
  //#endregion
3543
3547
  //#region src/db/migrations/022_scaler_manager_state.ts
3544
3548
  var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3545
- down: () => down$16,
3546
- up: () => up$16
3549
+ down: () => down$20,
3550
+ up: () => up$20
3547
3551
  });
3548
3552
  /**
3549
3553
  * Add three tables persisting `ScalerManager` per-coord state:
@@ -3570,7 +3574,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3570
3574
  * Idempotent: a re-run on a DB that already has any of these tables
3571
3575
  * leaves the existing one alone.
3572
3576
  */
3573
- async function up$16(db) {
3577
+ async function up$20(db) {
3574
3578
  const tableExists = async (name) => {
3575
3579
  return (await sql`
3576
3580
  SELECT EXISTS (
@@ -3620,7 +3624,7 @@ async function up$16(db) {
3620
3624
  `.execute(db);
3621
3625
  }
3622
3626
  }
3623
- async function down$16(db) {
3627
+ async function down$20(db) {
3624
3628
  await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
3625
3629
  await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
3626
3630
  await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
@@ -3628,8 +3632,8 @@ async function down$16(db) {
3628
3632
  //#endregion
3629
3633
  //#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
3630
3634
  var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
3631
- down: () => down$15,
3632
- up: () => up$15
3635
+ down: () => down$19,
3636
+ up: () => up$19
3633
3637
  });
3634
3638
  /**
3635
3639
  * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
@@ -3653,7 +3657,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
3653
3657
  * Idempotent: re-running on a DB that already has either column is a
3654
3658
  * no-op.
3655
3659
  */
3656
- async function up$15(db) {
3660
+ async function up$19(db) {
3657
3661
  const colExists = async (name) => {
3658
3662
  return (await sql`
3659
3663
  SELECT EXISTS (
@@ -3678,7 +3682,7 @@ async function up$15(db) {
3678
3682
  WHERE recovery_deadline IS NOT NULL
3679
3683
  `.execute(db);
3680
3684
  }
3681
- async function down$15(db) {
3685
+ async function down$19(db) {
3682
3686
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
3683
3687
  await sql`
3684
3688
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
@@ -3690,8 +3694,8 @@ async function down$15(db) {
3690
3694
  //#endregion
3691
3695
  //#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
3692
3696
  var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
3693
- down: () => down$14,
3694
- up: () => up$14
3697
+ down: () => down$18,
3698
+ up: () => up$18
3695
3699
  });
3696
3700
  /**
3697
3701
  * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
@@ -3707,7 +3711,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
3707
3711
  *
3708
3712
  * Idempotent: re-running on a DB that already has the column is a no-op.
3709
3713
  */
3710
- async function up$14(db) {
3714
+ async function up$18(db) {
3711
3715
  const colExists = async (name) => {
3712
3716
  return (await sql`
3713
3717
  SELECT EXISTS (
@@ -3723,7 +3727,7 @@ async function up$14(db) {
3723
3727
  ADD COLUMN last_provisioning_error TEXT
3724
3728
  `.execute(db);
3725
3729
  }
3726
- async function down$14(db) {
3730
+ async function down$18(db) {
3727
3731
  await sql`
3728
3732
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
3729
3733
  `.execute(db);
@@ -3731,8 +3735,8 @@ async function down$14(db) {
3731
3735
  //#endregion
3732
3736
  //#region src/db/migrations/025_init_failure.ts
3733
3737
  var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3734
- down: () => down$13,
3735
- up: () => up$13
3738
+ down: () => down$17,
3739
+ up: () => up$17
3736
3740
  });
3737
3741
  /**
3738
3742
  * Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
@@ -3746,7 +3750,7 @@ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3746
3750
  * Idempotent: re-running on a DB that already has either column is a no-op
3747
3751
  * for that column.
3748
3752
  */
3749
- async function up$13(db) {
3753
+ async function up$17(db) {
3750
3754
  const colExists = async (table, name) => {
3751
3755
  return (await sql`
3752
3756
  SELECT EXISTS (
@@ -3766,7 +3770,7 @@ async function up$13(db) {
3766
3770
  ADD COLUMN init_failure JSONB DEFAULT NULL
3767
3771
  `.execute(db);
3768
3772
  }
3769
- async function down$13(db) {
3773
+ async function down$17(db) {
3770
3774
  await sql`
3771
3775
  ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
3772
3776
  `.execute(db);
@@ -3777,8 +3781,8 @@ async function down$13(db) {
3777
3781
  //#endregion
3778
3782
  //#region src/db/migrations/026_event_log_lockfile_corrupt.ts
3779
3783
  var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3780
- down: () => down$12,
3781
- up: () => up$12
3784
+ down: () => down$16,
3785
+ up: () => up$16
3782
3786
  });
3783
3787
  /**
3784
3788
  * Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
@@ -3787,7 +3791,7 @@ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3787
3791
  *
3788
3792
  * Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
3789
3793
  */
3790
- async function up$12(db) {
3794
+ async function up$16(db) {
3791
3795
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3792
3796
  await sql`
3793
3797
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3797,7 +3801,7 @@ async function up$12(db) {
3797
3801
  ])))
3798
3802
  `.execute(db);
3799
3803
  }
3800
- async function down$12(db) {
3804
+ async function down$16(db) {
3801
3805
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3802
3806
  await sql`
3803
3807
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3810,8 +3814,8 @@ async function down$12(db) {
3810
3814
  //#endregion
3811
3815
  //#region src/db/migrations/027_workflow_timeout.ts
3812
3816
  var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3813
- down: () => down$11,
3814
- up: () => up$11
3817
+ down: () => down$15,
3818
+ up: () => up$15
3815
3819
  });
3816
3820
  /**
3817
3821
  * Add `workflow_timeout_ms integer` to `execution_runs`.
@@ -3829,13 +3833,13 @@ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3829
3833
  *
3830
3834
  * Idempotent: re-running on a DB that already has the column is a no-op.
3831
3835
  */
3832
- async function up$11(db) {
3836
+ async function up$15(db) {
3833
3837
  await sql`
3834
3838
  ALTER TABLE public.execution_runs
3835
3839
  ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
3836
3840
  `.execute(db);
3837
3841
  }
3838
- async function down$11(db) {
3842
+ async function down$15(db) {
3839
3843
  await sql`
3840
3844
  ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
3841
3845
  `.execute(db);
@@ -3843,8 +3847,8 @@ async function down$11(db) {
3843
3847
  //#endregion
3844
3848
  //#region src/db/migrations/028_org_settings_user_cache.ts
3845
3849
  var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
3846
- down: () => down$10,
3847
- up: () => up$10
3850
+ down: () => down$14,
3851
+ up: () => up$14
3848
3852
  });
3849
3853
  /**
3850
3854
  * Add `org_settings.user_cache_quota_bytes bigint` and
@@ -3873,7 +3877,7 @@ async function columnExists(db, column) {
3873
3877
  ) AS exists
3874
3878
  `.execute(db)).rows[0]?.exists ?? false;
3875
3879
  }
3876
- async function up$10(db) {
3880
+ async function up$14(db) {
3877
3881
  if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
3878
3882
  ALTER TABLE public.org_settings
3879
3883
  ADD COLUMN user_cache_quota_bytes bigint
@@ -3883,7 +3887,7 @@ async function up$10(db) {
3883
3887
  ADD COLUMN user_cache_ttl_ms bigint
3884
3888
  `.execute(db);
3885
3889
  }
3886
- async function down$10(db) {
3890
+ async function down$14(db) {
3887
3891
  await sql`
3888
3892
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
3889
3893
  `.execute(db);
@@ -3894,8 +3898,8 @@ async function down$10(db) {
3894
3898
  //#endregion
3895
3899
  //#region src/db/migrations/029_dispatch_queue_attempts.ts
3896
3900
  var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3897
- down: () => down$9,
3898
- up: () => up$9
3901
+ down: () => down$13,
3902
+ up: () => up$13
3899
3903
  });
3900
3904
  /**
3901
3905
  * Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
@@ -3909,7 +3913,7 @@ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3909
3913
  *
3910
3914
  * Idempotent: re-running on a DB that already has the column is a no-op.
3911
3915
  */
3912
- async function up$9(db) {
3916
+ async function up$13(db) {
3913
3917
  if (!((await sql`
3914
3918
  SELECT EXISTS (
3915
3919
  SELECT 1 FROM information_schema.columns
@@ -3922,7 +3926,7 @@ async function up$9(db) {
3922
3926
  ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
3923
3927
  `.execute(db);
3924
3928
  }
3925
- async function down$9(db) {
3929
+ async function down$13(db) {
3926
3930
  await sql`
3927
3931
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
3928
3932
  `.execute(db);
@@ -3930,8 +3934,8 @@ async function down$9(db) {
3930
3934
  //#endregion
3931
3935
  //#region src/db/migrations/030_held_runs_env_set_null.ts
3932
3936
  var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3933
- down: () => down$8,
3934
- up: () => up$8
3937
+ down: () => down$12,
3938
+ up: () => up$12
3935
3939
  });
3936
3940
  /**
3937
3941
  * held_runs.environment_id becomes nullable with ON DELETE SET NULL so
@@ -3942,14 +3946,14 @@ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3942
3946
  * Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
3943
3947
  * re-run, and the constraint is re-created with the SET NULL action.
3944
3948
  */
3945
- async function up$8(db) {
3949
+ async function up$12(db) {
3946
3950
  await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
3947
3951
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3948
3952
  await sql`ALTER TABLE public.held_runs
3949
3953
  ADD CONSTRAINT held_runs_environment_id_fkey
3950
3954
  FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
3951
3955
  }
3952
- async function down$8(db) {
3956
+ async function down$12(db) {
3953
3957
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3954
3958
  await sql`ALTER TABLE public.held_runs
3955
3959
  ADD CONSTRAINT held_runs_environment_id_fkey
@@ -3959,8 +3963,8 @@ async function down$8(db) {
3959
3963
  //#endregion
3960
3964
  //#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
3961
3965
  var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3962
- down: () => down$7,
3963
- up: () => up$7
3966
+ down: () => down$11,
3967
+ up: () => up$11
3964
3968
  });
3965
3969
  /**
3966
3970
  * Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
@@ -3977,7 +3981,7 @@ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3977
3981
  *
3978
3982
  * Idempotent: re-running on a DB that already has either column is a no-op.
3979
3983
  */
3980
- async function up$7(db) {
3984
+ async function up$11(db) {
3981
3985
  const colExists = async (name) => {
3982
3986
  return (await sql`
3983
3987
  SELECT EXISTS (
@@ -4002,7 +4006,7 @@ async function up$7(db) {
4002
4006
  WHERE ack_deadline IS NOT NULL
4003
4007
  `.execute(db);
4004
4008
  }
4005
- async function down$7(db) {
4009
+ async function down$11(db) {
4006
4010
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
4007
4011
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
4008
4012
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
@@ -4010,8 +4014,8 @@ async function down$7(db) {
4010
4014
  //#endregion
4011
4015
  //#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
4012
4016
  var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
4013
- down: () => down$6,
4014
- up: () => up$6
4017
+ down: () => down$10,
4018
+ up: () => up$10
4015
4019
  });
4016
4020
  /**
4017
4021
  * Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
@@ -4023,7 +4027,7 @@ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll
4023
4027
  *
4024
4028
  * Idempotent: a re-run on a DB that already has the column is a no-op.
4025
4029
  */
4026
- async function up$6(db) {
4030
+ async function up$10(db) {
4027
4031
  if ((await sql`
4028
4032
  SELECT EXISTS (
4029
4033
  SELECT 1 FROM information_schema.columns
@@ -4037,7 +4041,7 @@ async function up$6(db) {
4037
4041
  ADD COLUMN dispatch_ack_timeout_ms BIGINT
4038
4042
  `.execute(db);
4039
4043
  }
4040
- async function down$6(db) {
4044
+ async function down$10(db) {
4041
4045
  await sql`
4042
4046
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
4043
4047
  `.execute(db);
@@ -4045,8 +4049,8 @@ async function down$6(db) {
4045
4049
  //#endregion
4046
4050
  //#region src/db/migrations/033_org_settings_approval.ts
4047
4051
  var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4048
- down: () => down$5,
4049
- up: () => up$5
4052
+ down: () => down$9,
4053
+ up: () => up$9
4050
4054
  });
4051
4055
  /**
4052
4056
  * Add the two approval-policy columns to `org_settings`:
@@ -4063,7 +4067,7 @@ var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4063
4067
  * and the orchestrator admin route. Idempotent: a re-run on a DB that already
4064
4068
  * has the columns is a no-op (each column is guarded independently).
4065
4069
  */
4066
- async function up$5(db) {
4070
+ async function up$9(db) {
4067
4071
  const colExists = async (column) => {
4068
4072
  return (await sql`
4069
4073
  SELECT EXISTS (
@@ -4083,7 +4087,7 @@ async function up$5(db) {
4083
4087
  ADD COLUMN allow_self_approval BOOLEAN NOT NULL DEFAULT true
4084
4088
  `.execute(db);
4085
4089
  }
4086
- async function down$5(db) {
4090
+ async function down$9(db) {
4087
4091
  await sql`
4088
4092
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS approval_expiry_seconds
4089
4093
  `.execute(db);
@@ -4094,8 +4098,8 @@ async function down$5(db) {
4094
4098
  //#endregion
4095
4099
  //#region src/db/migrations/034_held_runs_generalize.ts
4096
4100
  var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4097
- down: () => down$4,
4098
- up: () => up$4
4101
+ down: () => down$8,
4102
+ up: () => up$8
4099
4103
  });
4100
4104
  /**
4101
4105
  * Generalize `held_runs` from an environment-only hold into the unified
@@ -4117,7 +4121,7 @@ var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4117
4121
  * New `held_run_approvals` table: one row per approver decision, FK to
4118
4122
  * `held_runs.id` (uuid) with ON DELETE CASCADE.
4119
4123
  */
4120
- async function up$4(db) {
4124
+ async function up$8(db) {
4121
4125
  const colExists = async (column) => {
4122
4126
  return (await sql`
4123
4127
  SELECT EXISTS (
@@ -4150,7 +4154,7 @@ async function up$4(db) {
4150
4154
  ON public.held_run_approvals USING btree (held_run_id)
4151
4155
  `.execute(db);
4152
4156
  }
4153
- async function down$4(db) {
4157
+ async function down$8(db) {
4154
4158
  await sql`DROP TABLE IF EXISTS public.held_run_approvals`.execute(db);
4155
4159
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS approval_requirement`.execute(db);
4156
4160
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS trigger_source`.execute(db);
@@ -4160,8 +4164,8 @@ async function down$4(db) {
4160
4164
  //#endregion
4161
4165
  //#region src/db/migrations/035_pending_workflow_contexts.ts
4162
4166
  var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4163
- down: () => down$3,
4164
- up: () => up$3
4167
+ down: () => down$7,
4168
+ up: () => up$7
4165
4169
  });
4166
4170
  /**
4167
4171
  * Pending workflow dispatch context — backs resume of a workflow whose install
@@ -4170,7 +4174,7 @@ var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4170
4174
  * wait-timer expiry, concurrency slot free). The row is deleted once the resume
4171
4175
  * dispatch has been kicked off.
4172
4176
  */
4173
- async function up$3(db) {
4177
+ async function up$7(db) {
4174
4178
  await sql`
4175
4179
  CREATE TABLE IF NOT EXISTS public.pending_workflow_contexts (
4176
4180
  run_id text PRIMARY KEY,
@@ -4180,14 +4184,14 @@ async function up$3(db) {
4180
4184
  )
4181
4185
  `.execute(db);
4182
4186
  }
4183
- async function down$3(db) {
4187
+ async function down$7(db) {
4184
4188
  await sql`DROP TABLE IF EXISTS public.pending_workflow_contexts`.execute(db);
4185
4189
  }
4186
4190
  //#endregion
4187
4191
  //#region src/db/migrations/036_attestations.ts
4188
4192
  var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4189
- down: () => down$2,
4190
- up: () => up$2
4193
+ down: () => down$6,
4194
+ up: () => up$6
4191
4195
  });
4192
4196
  /**
4193
4197
  * Add the `attestations` table for build-provenance bundles.
@@ -4200,7 +4204,7 @@ var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4200
4204
  *
4201
4205
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4202
4206
  */
4203
- async function up$2(db) {
4207
+ async function up$6(db) {
4204
4208
  if ((await sql`
4205
4209
  SELECT EXISTS (
4206
4210
  SELECT 1 FROM information_schema.tables
@@ -4226,14 +4230,14 @@ async function up$2(db) {
4226
4230
  ON public.attestations (run_id, job_id)
4227
4231
  `.execute(db);
4228
4232
  }
4229
- async function down$2(db) {
4233
+ async function down$6(db) {
4230
4234
  await sql`DROP TABLE IF EXISTS public.attestations`.execute(db);
4231
4235
  }
4232
4236
  //#endregion
4233
4237
  //#region src/db/migrations/037_generic_sources_provider_type_local.ts
4234
4238
  var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportAll({
4235
- down: () => down$1,
4236
- up: () => up$1
4239
+ down: () => down$5,
4240
+ up: () => up$5
4237
4241
  });
4238
4242
  /**
4239
4243
  * Replace the `generic_webhook_sources.provider_type` CHECK constraint so it
@@ -4250,7 +4254,7 @@ var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportA
4250
4254
  * Idempotent: the constraint is dropped IF EXISTS and recreated; the data
4251
4255
  * backfill is a plain UPDATE that is a no-op once no `'internal'` rows remain.
4252
4256
  */
4253
- async function up$1(db) {
4257
+ async function up$5(db) {
4254
4258
  await sql`
4255
4259
  ALTER TABLE public.generic_webhook_sources
4256
4260
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4266,7 +4270,7 @@ async function up$1(db) {
4266
4270
  CHECK (provider_type = ANY (ARRAY['generic'::text, 'local'::text]))
4267
4271
  `.execute(db);
4268
4272
  }
4269
- async function down$1(db) {
4273
+ async function down$5(db) {
4270
4274
  await sql`
4271
4275
  ALTER TABLE public.generic_webhook_sources
4272
4276
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4285,8 +4289,8 @@ async function down$1(db) {
4285
4289
  //#endregion
4286
4290
  //#region src/db/migrations/038_remote_sources.ts
4287
4291
  var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4288
- down: () => down,
4289
- up: () => up
4292
+ down: () => down$4,
4293
+ up: () => up$4
4290
4294
  });
4291
4295
  /**
4292
4296
  * `remote_sources` anchors a Platform-relayed `kici run remote` to its real
@@ -4298,7 +4302,7 @@ var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4298
4302
  *
4299
4303
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4300
4304
  */
4301
- async function up(db) {
4305
+ async function up$4(db) {
4302
4306
  if ((await sql`
4303
4307
  SELECT EXISTS (
4304
4308
  SELECT 1 FROM information_schema.tables
@@ -4317,10 +4321,200 @@ async function up(db) {
4317
4321
  )
4318
4322
  `.execute(db);
4319
4323
  }
4320
- async function down(db) {
4324
+ async function down$4(db) {
4321
4325
  await sql`DROP TABLE IF EXISTS public.remote_sources`.execute(db);
4322
4326
  }
4323
4327
  //#endregion
4328
+ //#region src/db/migrations/039_host_roster.ts
4329
+ var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4330
+ down: () => down$3,
4331
+ up: () => up$3
4332
+ });
4333
+ /**
4334
+ * `host_roster` is KiCI's declared inventory: one durable row per agent the
4335
+ * cluster has ever enrolled, reconciled from the in-memory AgentRegistry on
4336
+ * every register/unregister. `lifecycle_class` (snapshot of the auth token's
4337
+ * agent_type) drives reaping — `ephemeral` rows are GC'd past their TTL,
4338
+ * `static` rows persist and read as `unreachable` when their heartbeat goes
4339
+ * stale. `connected_instance_id` records which orchestrator holds the live WS
4340
+ * (cluster liveness + the host-fanout reroute target); NULL = disconnected.
4341
+ *
4342
+ * The roster lives in the shared cluster DB (one table, all instances). Status
4343
+ * is derived at read from the shared `last_seen` + `connected_instance_id`, so
4344
+ * every instance agrees regardless of which one holds the agent's live WS.
4345
+ *
4346
+ * Idempotent: a re-run on a DB that already has the table is a no-op.
4347
+ */
4348
+ async function up$3(db) {
4349
+ if ((await sql`
4350
+ SELECT EXISTS (
4351
+ SELECT 1 FROM information_schema.tables
4352
+ WHERE table_schema = 'public' AND table_name = 'host_roster'
4353
+ ) AS exists
4354
+ `.execute(db)).rows[0]?.exists) return;
4355
+ await sql`
4356
+ CREATE TABLE public.host_roster (
4357
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4358
+ agent_id text NOT NULL,
4359
+ token_id uuid,
4360
+ lifecycle_class text NOT NULL,
4361
+ labels text NOT NULL DEFAULT '[]',
4362
+ hostname text,
4363
+ platform text,
4364
+ arch text,
4365
+ connected_instance_id text,
4366
+ last_seen timestamptz NOT NULL DEFAULT now(),
4367
+ created_at timestamptz NOT NULL DEFAULT now(),
4368
+ updated_at timestamptz NOT NULL DEFAULT now(),
4369
+ CONSTRAINT host_roster_pkey PRIMARY KEY (id),
4370
+ CONSTRAINT host_roster_agent_id_key UNIQUE (agent_id),
4371
+ CONSTRAINT host_roster_lifecycle_class_check
4372
+ CHECK (lifecycle_class = ANY (ARRAY['static'::text, 'ephemeral'::text]))
4373
+ )
4374
+ `.execute(db);
4375
+ await sql`CREATE INDEX idx_host_roster_reap
4376
+ ON public.host_roster (lifecycle_class, last_seen)`.execute(db);
4377
+ }
4378
+ async function down$3(db) {
4379
+ await sql`DROP TABLE IF EXISTS public.host_roster`.execute(db);
4380
+ }
4381
+ //#endregion
4382
+ //#region src/db/migrations/040_runsonall_pin.ts
4383
+ var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4384
+ down: () => down$2,
4385
+ up: () => up$2
4386
+ });
4387
+ /**
4388
+ * Add the `runsOnAll` host fan-out columns:
4389
+ *
4390
+ * - `dispatch_queue.pinned_agent_id TEXT` — when set, the queued/dispatched job
4391
+ * targets exactly that agent (a host-fanout child). The dispatcher routes it
4392
+ * only to that agent; the queue drain never hands it to a different one.
4393
+ * - `execution_jobs.base_job_name` / `variant_kind` / `variant_label` — generic
4394
+ * fan-out columns (matrix + host uniform). `variant_kind` is `'matrix'` or
4395
+ * `'host'`; `variant_label` is the matrix suffix or the hostname. They make the
4396
+ * logical fan-out job first-class server-side so the dashboard groups on real
4397
+ * fields instead of string-parsing the job name. (`matrix_values` / `group_name`
4398
+ * already exist; the matrix path now also backfills `variant_kind='matrix'`.)
4399
+ *
4400
+ * Idempotent: re-running on a DB that already has any column is a no-op.
4401
+ */
4402
+ async function colExists$2(db, table, name) {
4403
+ return (await sql`
4404
+ SELECT EXISTS (
4405
+ SELECT 1 FROM information_schema.columns
4406
+ WHERE table_schema = 'public'
4407
+ AND table_name = ${table}
4408
+ AND column_name = ${name}
4409
+ ) AS exists
4410
+ `.execute(db)).rows[0]?.exists ?? false;
4411
+ }
4412
+ async function up$2(db) {
4413
+ if (!await colExists$2(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
4414
+ if (!await colExists$2(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
4415
+ if (!await colExists$2(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4416
+ if (!await colExists$2(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4417
+ await sql`
4418
+ CREATE INDEX IF NOT EXISTS idx_dispatch_queue_pinned_agent
4419
+ ON public.dispatch_queue (pinned_agent_id)
4420
+ WHERE pinned_agent_id IS NOT NULL
4421
+ `.execute(db);
4422
+ }
4423
+ async function down$2(db) {
4424
+ await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_pinned_agent`.execute(db);
4425
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS pinned_agent_id`.execute(db);
4426
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_label`.execute(db);
4427
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_kind`.execute(db);
4428
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS base_job_name`.execute(db);
4429
+ }
4430
+ //#endregion
4431
+ //#region src/db/migrations/041_wave_gated.ts
4432
+ var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4433
+ down: () => down$1,
4434
+ up: () => up$1
4435
+ });
4436
+ /**
4437
+ * Add the rolling fan-out wave-gate columns:
4438
+ *
4439
+ * - `execution_jobs.wave_gated boolean NOT NULL DEFAULT false` — when a fan-out
4440
+ * job declares `maxParallel`, children beyond the sliding window are persisted
4441
+ * `wave_gated=true` (held, not enqueued). The dispatch loop skips them; the
4442
+ * wave-scheduler clears the flag one-per-terminal as siblings complete (or, on
4443
+ * `failFast`, skips the held remainder).
4444
+ * - `execution_jobs.wave_max_parallel int` / `wave_fail_fast boolean` — the
4445
+ * base's wave policy, stamped on every fan-out child so the wave-scheduler can
4446
+ * read it at terminal time without re-fetching the lock file (the tracker has
4447
+ * no lock access). NULL for any job not part of a bounded wave.
4448
+ *
4449
+ * A composite index on (run_id, base_job_name, wave_gated) supports the
4450
+ * wave-scheduler's "next held sibling of this base" lookups.
4451
+ *
4452
+ * Idempotent: re-running on a DB that already has the columns is a no-op.
4453
+ */
4454
+ async function colExists$1(db, table, name) {
4455
+ return (await sql`
4456
+ SELECT EXISTS (
4457
+ SELECT 1 FROM information_schema.columns
4458
+ WHERE table_schema = 'public'
4459
+ AND table_name = ${table}
4460
+ AND column_name = ${name}
4461
+ ) AS exists
4462
+ `.execute(db)).rows[0]?.exists ?? false;
4463
+ }
4464
+ async function up$1(db) {
4465
+ if (!await colExists$1(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
4466
+ if (!await colExists$1(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
4467
+ if (!await colExists$1(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4468
+ await sql`
4469
+ CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
4470
+ ON public.execution_jobs (run_id, base_job_name, wave_gated)
4471
+ `.execute(db);
4472
+ }
4473
+ async function down$1(db) {
4474
+ await sql`DROP INDEX IF EXISTS public.idx_execution_jobs_wave`.execute(db);
4475
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
4476
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
4477
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_gated`.execute(db);
4478
+ }
4479
+ //#endregion
4480
+ //#region src/db/migrations/042_dispatch_queue_patterns.ts
4481
+ var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4482
+ down: () => down,
4483
+ up: () => up
4484
+ });
4485
+ /**
4486
+ * Add pattern columns to dispatch_queue. Exact labels stay in runs_on_labels /
4487
+ * exclude_labels (the SQL @> prefilter); regex matchers go here and are applied
4488
+ * as a JS post-filter at drain time, since Postgres `~` regex semantics differ
4489
+ * from JavaScript `RegExp` and the engine's `matcherSatisfiedBy` is the single
4490
+ * matching authority.
4491
+ *
4492
+ * - `runs_on_patterns jsonb NOT NULL DEFAULT '[]'` — regex matchers the agent's
4493
+ * labels must satisfy.
4494
+ * - `exclude_patterns jsonb NOT NULL DEFAULT '[]'` — regex matchers that
4495
+ * disqualify an agent.
4496
+ *
4497
+ * Idempotent: re-running on a DB that already has the columns is a no-op.
4498
+ */
4499
+ async function colExists(db, table, name) {
4500
+ return (await sql`
4501
+ SELECT EXISTS (
4502
+ SELECT 1 FROM information_schema.columns
4503
+ WHERE table_schema = 'public'
4504
+ AND table_name = ${table}
4505
+ AND column_name = ${name}
4506
+ ) AS exists
4507
+ `.execute(db)).rows[0]?.exists ?? false;
4508
+ }
4509
+ async function up(db) {
4510
+ if (!await colExists(db, "dispatch_queue", "runs_on_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN runs_on_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4511
+ if (!await colExists(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4512
+ }
4513
+ async function down(db) {
4514
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
4515
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
4516
+ }
4517
+ //#endregion
4324
4518
  //#region src/db/migration-provider.ts
4325
4519
  function createMigrationProvider() {
4326
4520
  return { async getMigrations() {
@@ -4362,7 +4556,11 @@ function createMigrationProvider() {
4362
4556
  "035_pending_workflow_contexts": _035_pending_workflow_contexts_exports,
4363
4557
  "036_attestations": _036_attestations_exports,
4364
4558
  "037_generic_sources_provider_type_local": _037_generic_sources_provider_type_local_exports,
4365
- "038_remote_sources": _038_remote_sources_exports
4559
+ "038_remote_sources": _038_remote_sources_exports,
4560
+ "039_host_roster": _039_host_roster_exports,
4561
+ "040_runsonall_pin": _040_runsonall_pin_exports,
4562
+ "041_wave_gated": _041_wave_gated_exports,
4563
+ "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports
4366
4564
  };
4367
4565
  } };
4368
4566
  }
@@ -6631,7 +6829,7 @@ function registerDebugBundleCommand(program, getClient) {
6631
6829
  safeRequest(() => client.getText("/metrics"))
6632
6830
  ]);
6633
6831
  const output = fs$1.createWriteStream(outputPath);
6634
- const archive = archiver("zip", { zlib: { level: 6 } });
6832
+ const archive = new ZipArchive({ zlib: { level: 6 } });
6635
6833
  const finalized = new Promise((resolve, reject) => {
6636
6834
  output.on("close", resolve);
6637
6835
  archive.on("error", reject);
@@ -6958,8 +7156,18 @@ function isRoot() {
6958
7156
  *
6959
7157
  * Used by the instance index (`<kiciRoot>/instances.json`) which lives
6960
7158
  * outside any per-instance subdir, and as the base for {@link getConfigDir}.
7159
+ *
7160
+ * `KICI_CONFIG_ROOT` overrides the platform default for both privilege scopes.
7161
+ * It exists so a test harness (or a sandboxed run) can point the instance index
7162
+ * at an isolated directory instead of sharing the host's `~/.config/kici/` —
7163
+ * which otherwise lets concurrent installs clobber each other's index.
6961
7164
  */
6962
7165
  function kiciConfigRoot(isUserLevel) {
7166
+ const override = process.env.KICI_CONFIG_ROOT;
7167
+ if (override) {
7168
+ const sep = os.platform() === "win32" ? "\\" : "/";
7169
+ return override.endsWith(sep) ? override : override + sep;
7170
+ }
6963
7171
  const plat = os.platform();
6964
7172
  if (plat === "win32") {
6965
7173
  if (isUserLevel) return (process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local")) + "\\kici\\";
@@ -7107,6 +7315,37 @@ function readKiciVersion() {
7107
7315
  return "unknown";
7108
7316
  }
7109
7317
  }
7318
+ /**
7319
+ * Resolve the `@kici-dev/<component>` package version that a {@link LaunchSpec}
7320
+ * will actually execute, by locating the component's entry script among the
7321
+ * spec's tokens and reading the version from the package.json that owns it.
7322
+ *
7323
+ * Returns null when no resolvable entry script is present (e.g. an opaque
7324
+ * `--binary` install) or the owning package.json is missing / name-mismatched.
7325
+ * This is what lets the npm-source upgrade refuse to report a version it can't
7326
+ * stand behind.
7327
+ */
7328
+ function resolveVersionFromLaunchSpec(spec, component) {
7329
+ const pkgName = `@kici-dev/${component}`;
7330
+ const entryRe = new RegExp(`[/\\\\]@kici-dev[/\\\\]${component}[/\\\\]dist[/\\\\](?:server|standalone)\\.js$`);
7331
+ const entry = [spec.execPath, ...spec.args].find((a) => entryRe.test(a));
7332
+ if (!entry) return null;
7333
+ let dir = path.dirname(entry);
7334
+ for (let i = 0; i < 6; i++) {
7335
+ const candidate = path.join(dir, "package.json");
7336
+ if (fs.existsSync(candidate)) try {
7337
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
7338
+ if (pkg.name === pkgName && typeof pkg.version === "string") return pkg.version;
7339
+ return null;
7340
+ } catch {
7341
+ return null;
7342
+ }
7343
+ const parent = path.dirname(dir);
7344
+ if (parent === dir) break;
7345
+ dir = parent;
7346
+ }
7347
+ return null;
7348
+ }
7110
7349
  //#endregion
7111
7350
  //#region src/cli/service/instance/index-file.ts
7112
7351
  /**
@@ -7184,27 +7423,54 @@ function removeIndexEntry(kiciRoot, key) {
7184
7423
  */
7185
7424
  /**
7186
7425
  * Reconcile <kiciRoot>/instances.json with the driver's native scan, then
7187
- * rewrite the index dropping entries whose units no longer exist. Returns
7188
- * the merged list filtered to the requested component + isUserLevel.
7426
+ * rewrite the index so it mirrors the scanned instances that have a known
7427
+ * instanceDir. Two self-heal directions happen here:
7428
+ *
7429
+ * - backward: drop index entries whose unit no longer exists.
7430
+ * - forward: adopt an instanceDir the driver recovered from the unit marker
7431
+ * when the index entry is missing or carries no dir — so a lost or emptied
7432
+ * index rebuilds itself from the init system on the next read.
7433
+ *
7434
+ * The init system is therefore the source of truth for the name→folder mapping
7435
+ * (via the X-KiCI-InstanceDir / KiCIInstanceDir / dev.kici.instance-dir / [KiCI-DIR]
7436
+ * markers), and the index is a pure cache. Returns the merged list filtered to
7437
+ * the requested component + isUserLevel.
7189
7438
  */
7190
7439
  async function listInstances(args) {
7191
7440
  const { component, isUserLevel, kiciRoot, manager } = args;
7192
7441
  const scanForComponent = (await manager.list(isUserLevel)).filter((s) => s.component === component);
7193
- const scanNames = new Set(scanForComponent.map((s) => s.name));
7194
7442
  const index = readIndex(kiciRoot);
7195
7443
  const relevantIndex = index.filter((e) => e.component === component && e.isUserLevel === isUserLevel);
7196
- const survivors = relevantIndex.filter((e) => scanNames.has(e.name));
7197
- if (survivors.length !== relevantIndex.length) writeIndex(kiciRoot, [...index.filter((e) => !(e.component === component && e.isUserLevel === isUserLevel)), ...survivors]);
7198
- const indexByName = new Map(survivors.map((e) => [e.name, e]));
7199
- return scanForComponent.map((s) => {
7444
+ const indexByName = new Map(relevantIndex.map((e) => [e.name, e]));
7445
+ const listed = scanForComponent.map((s) => {
7200
7446
  const idx = indexByName.get(s.name);
7201
7447
  return {
7202
7448
  ...s,
7203
7449
  component,
7204
- instanceDir: idx?.instanceDir,
7450
+ instanceDir: idx?.instanceDir ?? s.instanceDir,
7205
7451
  source: idx ? "index+scan" : "scan"
7206
7452
  };
7207
7453
  });
7454
+ const reconciled = [];
7455
+ for (const l of listed) {
7456
+ if (!l.instanceDir) continue;
7457
+ reconciled.push({
7458
+ component,
7459
+ name: l.name,
7460
+ platform: l.platform,
7461
+ isUserLevel,
7462
+ instanceDir: l.instanceDir
7463
+ });
7464
+ }
7465
+ if (!sameIndexSet(relevantIndex, reconciled)) writeIndex(kiciRoot, [...index.filter((e) => !(e.component === component && e.isUserLevel === isUserLevel)), ...reconciled]);
7466
+ return listed;
7467
+ }
7468
+ /** Order-insensitive equality of two index-entry sets (by their full identity). */
7469
+ function sameIndexSet(a, b) {
7470
+ if (a.length !== b.length) return false;
7471
+ const key = (e) => `${e.component}\0${e.name}\0${e.platform}\0${e.isUserLevel}\0${e.instanceDir}`;
7472
+ const setA = new Set(a.map(key));
7473
+ return b.every((e) => setA.has(key(e)));
7208
7474
  }
7209
7475
  /**
7210
7476
  * Resolve the target instance for the current lifecycle invocation.
@@ -7326,6 +7592,7 @@ var init_systemd = __esmMin((() => {
7326
7592
  lines.push("[Unit]");
7327
7593
  lines.push(`Description=${config.description}`);
7328
7594
  if (config.component) lines.push(`X-KiCI-Component=${config.component}`);
7595
+ if (config.instanceDir) lines.push(`X-KiCI-InstanceDir=${config.instanceDir}`);
7329
7596
  lines.push("After=network.target postgresql.service");
7330
7597
  lines.push("");
7331
7598
  lines.push("[Service]");
@@ -7462,6 +7729,22 @@ var init_systemd = __esmMin((() => {
7462
7729
  async isInstalled(config) {
7463
7730
  return fs.existsSync(this.unitFilePath(config));
7464
7731
  }
7732
+ async readLaunchSpec(config) {
7733
+ let content;
7734
+ try {
7735
+ content = fs.readFileSync(this.unitFilePath(config), "utf-8");
7736
+ } catch {
7737
+ return null;
7738
+ }
7739
+ const m = content.match(/^ExecStart=(.+?)\s*$/m);
7740
+ if (!m) return null;
7741
+ const tokens = m[1].trim().split(/\s+/).filter(Boolean);
7742
+ if (tokens.length === 0) return null;
7743
+ return {
7744
+ execPath: tokens[0],
7745
+ args: tokens.slice(1)
7746
+ };
7747
+ }
7465
7748
  async list(isUserLevel) {
7466
7749
  const unitDir = isUserLevel ? path.join(os.homedir(), ".config", "systemd", "user") : "/etc/systemd/system";
7467
7750
  if (!fs.existsSync(unitDir)) return [];
@@ -7469,13 +7752,16 @@ var init_systemd = __esmMin((() => {
7469
7752
  for (const entry of fs.readdirSync(unitDir)) {
7470
7753
  if (typeof entry !== "string") continue;
7471
7754
  if (!entry.startsWith("kici-") || !entry.endsWith(".service")) continue;
7472
- const m = fs.readFileSync(path.join(unitDir, entry), "utf-8").match(/^X-KiCI-Component=(orchestrator|agent)\s*$/m);
7755
+ const content = fs.readFileSync(path.join(unitDir, entry), "utf-8");
7756
+ const m = content.match(/^X-KiCI-Component=(orchestrator|agent)\s*$/m);
7473
7757
  if (!m) continue;
7758
+ const dirMatch = content.match(/^X-KiCI-InstanceDir=(.+?)\s*$/m);
7474
7759
  out.push({
7475
7760
  name: entry.replace(/\.service$/, ""),
7476
7761
  platform: "systemd",
7477
7762
  isUserLevel,
7478
- component: m[1]
7763
+ component: m[1],
7764
+ instanceDir: dirMatch?.[1]
7479
7765
  });
7480
7766
  }
7481
7767
  return out;
@@ -7534,6 +7820,10 @@ var init_launchd = __esmMin((() => {
7534
7820
  escapeXml(s) {
7535
7821
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7536
7822
  }
7823
+ /** Inverse of {@link escapeXml} — decode the entities back to literals. */
7824
+ unescapeXml(s) {
7825
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&amp;/g, "&");
7826
+ }
7537
7827
  /**
7538
7828
  * Generate a launchd plist XML string from a service config.
7539
7829
  * Visible for testing.
@@ -7553,6 +7843,10 @@ var init_launchd = __esmMin((() => {
7553
7843
  lines.push(" <key>KiCIComponent</key>");
7554
7844
  lines.push(` <string>${config.component}</string>`);
7555
7845
  }
7846
+ if (config.instanceDir) {
7847
+ lines.push(" <key>KiCIInstanceDir</key>");
7848
+ lines.push(` <string>${this.escapeXml(config.instanceDir)}</string>`);
7849
+ }
7556
7850
  lines.push(" <key>ProgramArguments</key>");
7557
7851
  lines.push(" <array>");
7558
7852
  lines.push(` <string>${this.escapeXml(config.executablePath)}</string>`);
@@ -7772,15 +8066,33 @@ var init_launchd = __esmMin((() => {
7772
8066
  }
7773
8067
  const match = content.match(/<key>KiCIComponent<\/key>\s*<string>(orchestrator|agent)<\/string>/);
7774
8068
  if (!match) continue;
8069
+ const dirMatch = content.match(/<key>KiCIInstanceDir<\/key>\s*<string>([^<]+)<\/string>/);
7775
8070
  out.push({
7776
8071
  name: entry.replace(/\.plist$/, ""),
7777
8072
  platform: "launchd",
7778
8073
  isUserLevel,
7779
- component: match[1]
8074
+ component: match[1],
8075
+ instanceDir: dirMatch ? this.unescapeXml(dirMatch[1]) : void 0
7780
8076
  });
7781
8077
  }
7782
8078
  return out;
7783
8079
  }
8080
+ async readLaunchSpec(config) {
8081
+ let content;
8082
+ try {
8083
+ content = fs.readFileSync(this.plistPath(config), "utf-8");
8084
+ } catch {
8085
+ return null;
8086
+ }
8087
+ const arr = content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
8088
+ if (!arr) return null;
8089
+ const strings = [...arr[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((s) => this.unescapeXml(s[1]));
8090
+ if (strings.length === 0) return null;
8091
+ return {
8092
+ execPath: strings[0],
8093
+ args: strings.slice(1)
8094
+ };
8095
+ }
7784
8096
  };
7785
8097
  }));
7786
8098
  //#endregion
@@ -7915,61 +8227,61 @@ var init_registry = __esmMin((() => {
7915
8227
  rolldown: {
7916
8228
  "linux-x64": {
7917
8229
  name: "rolldown",
7918
- version: "1.1.0",
8230
+ version: "1.1.1",
7919
8231
  platform: "linux",
7920
8232
  arch: "x64",
7921
- url: "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.0.tgz",
7922
- sha256: "07ccc910deab806eae00c93cfa633eeedfe5df92cabd0fc92111dd982ab3c324",
8233
+ url: "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.1.tgz",
8234
+ sha256: "55bf455e4d6c5df19636c40b434718891a574214bbf09a50e2e7b21b9f640d56",
7923
8235
  extractPath: "package/",
7924
8236
  archiveType: "tar.gz"
7925
8237
  },
7926
8238
  "linux-arm64": {
7927
8239
  name: "rolldown",
7928
- version: "1.1.0",
8240
+ version: "1.1.1",
7929
8241
  platform: "linux",
7930
8242
  arch: "arm64",
7931
- url: "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.0.tgz",
7932
- sha256: "e7e812642e292101c65703101e29622da59279c53cd68deefed2a356a76ceaf0",
8243
+ url: "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.1.tgz",
8244
+ sha256: "af458cfd2ce237a5161bdfdcef960e710c394b966537e1d1374840fb7fc78308",
7933
8245
  extractPath: "package/",
7934
8246
  archiveType: "tar.gz"
7935
8247
  },
7936
8248
  "darwin-x64": {
7937
8249
  name: "rolldown",
7938
- version: "1.1.0",
8250
+ version: "1.1.1",
7939
8251
  platform: "darwin",
7940
8252
  arch: "x64",
7941
- url: "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.0.tgz",
7942
- sha256: "48f52e0b62d87117f96c7660e2838dded1d6a99f45d56ae192ab49475a8fa9d1",
8253
+ url: "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.1.tgz",
8254
+ sha256: "31bd12457aa41646dcee674d7fa73ec2c4625ef5d34d0ef6c6823cad811de3e2",
7943
8255
  extractPath: "package/",
7944
8256
  archiveType: "tar.gz"
7945
8257
  },
7946
8258
  "darwin-arm64": {
7947
8259
  name: "rolldown",
7948
- version: "1.1.0",
8260
+ version: "1.1.1",
7949
8261
  platform: "darwin",
7950
8262
  arch: "arm64",
7951
- url: "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.0.tgz",
7952
- sha256: "c798f79bbcc28079af101ca323c194f2ad6472da0ae80721cc3eaa370e753f6a",
8263
+ url: "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.1.tgz",
8264
+ sha256: "8399e7a87640ffe485ba16289f6c3a7ab69b25d09b43e5896441372bcf6a02f9",
7953
8265
  extractPath: "package/",
7954
8266
  archiveType: "tar.gz"
7955
8267
  },
7956
8268
  "win32-x64": {
7957
8269
  name: "rolldown",
7958
- version: "1.1.0",
8270
+ version: "1.1.1",
7959
8271
  platform: "win32",
7960
8272
  arch: "x64",
7961
- url: "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.0.tgz",
7962
- sha256: "3457d231d8dcce922bf1cb8f696f04600378ce8e88c0898dba9999239d64dd4a",
8273
+ url: "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.1.tgz",
8274
+ sha256: "2b2c34d530db0e2cf15cdbe328822d5d385b19ee3b5aba55771587027b9afa17",
7963
8275
  extractPath: "package/",
7964
8276
  archiveType: "tar.gz"
7965
8277
  },
7966
8278
  "win32-arm64": {
7967
8279
  name: "rolldown",
7968
- version: "1.1.0",
8280
+ version: "1.1.1",
7969
8281
  platform: "win32",
7970
8282
  arch: "arm64",
7971
- url: "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.0.tgz",
7972
- sha256: "c8e9e9643f2e905ca0062d2e768c2966e48f0a47595c8c2b7658a085024c8ce3",
8283
+ url: "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.1.tgz",
8284
+ sha256: "d7431cb75d0bed42dd8794b89c3d73ac7129a36465ac020519726f2a9dca1d7c",
7973
8285
  extractPath: "package/",
7974
8286
  archiveType: "tar.gz"
7975
8287
  }
@@ -8078,7 +8390,8 @@ var init_windows = __esmMin((() => {
8078
8390
  cmdParts.push("--", `"${config.executablePath}"`);
8079
8391
  for (const arg of config.args ?? []) cmdParts.push(`"${arg}"`);
8080
8392
  execSync(cmdParts.join(" "), { stdio: "pipe" });
8081
- const descText = config.component ? `[KiCI:${config.component}] ${config.description}` : config.description;
8393
+ const dirSuffix = config.component && config.instanceDir ? ` [KiCI-DIR:${config.instanceDir}]` : "";
8394
+ const descText = config.component ? `[KiCI:${config.component}] ${config.description}${dirSuffix}` : config.description;
8082
8395
  execSync(`sc.exe description ${config.name} "${descText.replace(/"/g, "\\\"")}"`, { stdio: "pipe" });
8083
8396
  execSync(`sc.exe config ${config.name} start= auto`, { stdio: "pipe" });
8084
8397
  const actions = config.restartPolicy.delays.map((d) => `restart/${d * 1e3}`).join("/");
@@ -8189,18 +8502,39 @@ var init_windows = __esmMin((() => {
8189
8502
  for (const row of rows) {
8190
8503
  if (!row || typeof row !== "object") continue;
8191
8504
  const r = row;
8192
- const match = (typeof r.Description === "string" ? r.Description : "").match(/^\[KiCI:(orchestrator|agent)\]/);
8505
+ const desc = typeof r.Description === "string" ? r.Description : "";
8506
+ const match = desc.match(/^\[KiCI:(orchestrator|agent)\]/);
8193
8507
  if (!match) continue;
8194
8508
  if (typeof r.Name !== "string") continue;
8509
+ const dirMatch = desc.match(/\[KiCI-DIR:([^\]]+)\]/);
8195
8510
  out.push({
8196
8511
  name: r.Name,
8197
8512
  platform: "windows",
8198
8513
  isUserLevel,
8199
- component: match[1]
8514
+ component: match[1],
8515
+ instanceDir: dirMatch?.[1]
8200
8516
  });
8201
8517
  }
8202
8518
  return out;
8203
8519
  }
8520
+ async readLaunchSpec(config) {
8521
+ let raw;
8522
+ try {
8523
+ raw = execSync(`sc.exe qc ${config.name}`, { stdio: "pipe" }).toString();
8524
+ } catch {
8525
+ return null;
8526
+ }
8527
+ const m = raw.match(/BINARY_PATH_NAME\s*:\s*(.+)/);
8528
+ if (!m) return null;
8529
+ const sep = m[1].indexOf(" -- ");
8530
+ if (sep === -1) return null;
8531
+ const tokens = [...m[1].slice(sep + 4).matchAll(/"([^"]*)"|(\S+)/g)].map((t) => t[1] ?? t[2]);
8532
+ if (tokens.length === 0 || !tokens[0]) return null;
8533
+ return {
8534
+ execPath: tokens[0],
8535
+ args: tokens.slice(1)
8536
+ };
8537
+ }
8204
8538
  };
8205
8539
  }));
8206
8540
  //#endregion
@@ -8341,6 +8675,7 @@ function generateComposeYaml(config) {
8341
8675
  if (config.component) {
8342
8676
  lines.push(" labels:");
8343
8677
  lines.push(` dev.kici.component: ${config.component}`);
8678
+ if (config.instanceDir) lines.push(` dev.kici.instance-dir: "${config.instanceDir}"`);
8344
8679
  }
8345
8680
  return lines.join("\n") + "\n";
8346
8681
  }
@@ -8433,19 +8768,25 @@ var init_compose = __esmMin((() => {
8433
8768
  }
8434
8769
  if (!row || typeof row !== "object") continue;
8435
8770
  const r = row;
8436
- const m = (typeof r.Labels === "string" ? r.Labels : "").match(/dev\.kici\.component=(orchestrator|agent)/);
8771
+ const labels = typeof r.Labels === "string" ? r.Labels : "";
8772
+ const m = labels.match(/dev\.kici\.component=(orchestrator|agent)/);
8437
8773
  if (!m) continue;
8438
8774
  const name = typeof r.Names === "string" ? r.Names : String(r.Names ?? "");
8439
8775
  if (!name) continue;
8776
+ const dirMatch = labels.match(/dev\.kici\.instance-dir=([^,]+)/);
8440
8777
  out.push({
8441
8778
  name,
8442
8779
  platform: "compose",
8443
8780
  isUserLevel,
8444
- component: m[1]
8781
+ component: m[1],
8782
+ instanceDir: dirMatch?.[1]
8445
8783
  });
8446
8784
  }
8447
8785
  return out;
8448
8786
  }
8787
+ async readLaunchSpec(_config) {
8788
+ return null;
8789
+ }
8449
8790
  };
8450
8791
  }));
8451
8792
  //#endregion
@@ -8567,7 +8908,8 @@ async function resolveUpgradeTarget(args) {
8567
8908
  maxRetries: 5,
8568
8909
  windowSeconds: 300
8569
8910
  },
8570
- component
8911
+ component,
8912
+ instanceDir: resolved.instanceDir
8571
8913
  },
8572
8914
  installBase: resolved.manifest.installBase,
8573
8915
  resolvedInstance: resolved
@@ -8721,6 +9063,36 @@ function resolveNpmSourceVersion(opts) {
8721
9063
  if (opts.requested && opts.requested !== opts.running) throw new Error(`npm-source upgrade: requested --version ${opts.requested} does not match the installed package version ${opts.running}. Run \`npm install -g @kici-dev/<pkg>@${opts.requested}\` first, then re-run the upgrade.`);
8722
9064
  return opts.running;
8723
9065
  }
9066
+ /**
9067
+ * Decide whether an npm-source upgrade may proceed, given the version the
9068
+ * invoking CLI is (`invoked`) and the version the installed unit will actually
9069
+ * launch (`launched`, or null when unresolvable — e.g. an opaque --binary
9070
+ * install). On success `manifestVersion` is the value to persist (null = leave
9071
+ * the manifest's kiciVersion unchanged because we couldn't verify it).
9072
+ */
9073
+ function verifyNpmSourceLaunch(opts) {
9074
+ const { component, invoked, launched, launchedPath, force } = opts;
9075
+ if (launched === null) {
9076
+ if (force) return {
9077
+ ok: true,
9078
+ version: invoked,
9079
+ manifestVersion: null
9080
+ };
9081
+ return {
9082
+ ok: false,
9083
+ reason: "npm-source upgrade aborted: could not determine the version the installed unit will launch (custom --binary install or unparseable launch target). Pass --force to restart without version verification (the manifest version is left unchanged)."
9084
+ };
9085
+ }
9086
+ if (launched !== invoked) return {
9087
+ ok: false,
9088
+ reason: `npm-source upgrade aborted: this kici-admin is version ${invoked}, but the installed unit will launch version ${launched}${launchedPath ? ` (via ${launchedPath})` : ""}. Your \`npm install -g\` updated a different install than the service is pinned to. Install ${invoked} under the unit's runtime (the node its ExecStart points at), or re-run \`kici-admin ${component} install\` to repoint the unit, then retry.`
9089
+ };
9090
+ return {
9091
+ ok: true,
9092
+ version: launched,
9093
+ manifestVersion: launched
9094
+ };
9095
+ }
8724
9096
  async function performVersionedUpgrade(component, opts) {
8725
9097
  try {
8726
9098
  const platform = detectPlatform(opts.platform);
@@ -8750,7 +9122,7 @@ async function performVersionedUpgrade(component, opts) {
8750
9122
  return;
8751
9123
  }
8752
9124
  if (!opts.from && !opts.url) {
8753
- await performNpmSourceUpgrade(config, resolvedInstance, manager, opts);
9125
+ await performNpmSourceUpgrade(component, config, resolvedInstance, manager, opts);
8754
9126
  return;
8755
9127
  }
8756
9128
  if (!opts.version) {
@@ -8859,18 +9231,30 @@ async function performVersionedUpgrade(component, opts) {
8859
9231
  * package (see install.ts → import.meta.resolve), so a stop+start re-executes
8860
9232
  * the freshly-installed code. No archive, no versioned dir, no symlink.
8861
9233
  */
8862
- async function performNpmSourceUpgrade(config, resolvedInstance, manager, opts) {
8863
- const running = readKiciVersion();
8864
- const version = resolveNpmSourceVersion({
9234
+ async function performNpmSourceUpgrade(component, config, resolvedInstance, manager, opts) {
9235
+ const invoked = readKiciVersion();
9236
+ resolveNpmSourceVersion({
8865
9237
  requested: opts.version,
8866
- running
9238
+ running: invoked
8867
9239
  });
8868
9240
  if (!await manager.isInstalled(config)) {
8869
9241
  console.error(`Error: service "${config.name}" is not installed`);
8870
9242
  process.exit(1);
8871
9243
  }
9244
+ const spec = await manager.readLaunchSpec(config);
9245
+ const verdict = verifyNpmSourceLaunch({
9246
+ component,
9247
+ invoked,
9248
+ launched: spec ? resolveVersionFromLaunchSpec(spec, component) : null,
9249
+ launchedPath: spec?.execPath ?? null,
9250
+ force: opts.force ?? false
9251
+ });
9252
+ if (!verdict.ok) {
9253
+ console.error(`Error: ${verdict.reason}`);
9254
+ process.exit(1);
9255
+ }
8872
9256
  if (!opts.yes) {
8873
- console.log(`This will restart "${config.name}" onto the npm-installed version ${version}.`);
9257
+ console.log(`This will restart "${config.name}" onto the npm-installed version ${verdict.version}.`);
8874
9258
  console.log(" The service will be stopped briefly during the restart.");
8875
9259
  console.log("");
8876
9260
  if (!await confirm$2("Proceed with upgrade?")) {
@@ -8881,11 +9265,12 @@ async function performNpmSourceUpgrade(config, resolvedInstance, manager, opts)
8881
9265
  console.log("Restarting service onto the npm-installed package...");
8882
9266
  if ((await manager.status(config)).state === "running") await manager.stop(config);
8883
9267
  await manager.start(config);
8884
- writeManifest(resolvedInstance.instanceDir, {
9268
+ if (verdict.manifestVersion !== null) writeManifest(resolvedInstance.instanceDir, {
8885
9269
  ...resolvedInstance.manifest,
8886
- kiciVersion: version
9270
+ kiciVersion: verdict.manifestVersion
8887
9271
  });
8888
- console.log(`Upgrade complete: service "${config.name}" is now running version ${version}.`);
9272
+ else console.log("Note: manifest version left unchanged (verification was skipped via --force).");
9273
+ console.log(`Upgrade complete: service "${config.name}" is now running version ${verdict.version}.`);
8889
9274
  }
8890
9275
  /**
8891
9276
  * Handle --rollback: switch symlink to the previous version and restart.
@@ -9235,7 +9620,7 @@ function registerOrchestratorInstall(orchestrator) {
9235
9620
  })).find((c) => c.name === serviceName);
9236
9621
  if (existing && existing.instanceDir !== instanceDir && !opts.force) {
9237
9622
  const at = existing.instanceDir ?? "(no manifest)";
9238
- console.error(`Error: an orchestrator instance "${serviceName}" is already installed at ${at}. Pass a different --name, a different --instance-dir, or --force to overwrite.`);
9623
+ console.error(`Error: an orchestrator instance "${serviceName}" is already installed at ${at}.\n\nUpgrading this service? Don't re-run install — installing again is for first-time\nsetup, not upgrades. For an npm-global install run \`npm install -g kici-admin@latest\`\nthen \`kici-admin orchestrator restart\`; for a versioned-directory install run\n\`kici-admin orchestrator upgrade\`.\n\nInstalling a second, separate instance? Pass a different --name or --instance-dir,\nor --force to take over this one.`);
9239
9624
  process.exit(1);
9240
9625
  }
9241
9626
  const configDir = getConfigDir(serviceName, userLevel);
@@ -9304,6 +9689,7 @@ function registerOrchestratorInstall(orchestrator) {
9304
9689
  isUserLevel: userLevel,
9305
9690
  user: opts.user,
9306
9691
  component: "orchestrator",
9692
+ instanceDir,
9307
9693
  restartPolicy: DEFAULT_RESTART_POLICY
9308
9694
  };
9309
9695
  await manager.install(config);
@@ -9810,6 +10196,7 @@ function registerAgentInstall(agent) {
9810
10196
  workingDirectory: configDir,
9811
10197
  isUserLevel: userLevel,
9812
10198
  component: "agent",
10199
+ instanceDir,
9813
10200
  restartPolicy: DEFAULT_RESTART_POLICY
9814
10201
  };
9815
10202
  await manager.install(config);
@@ -10602,6 +10989,211 @@ function registerPeerCommands(program, _getClient) {
10602
10989
  }
10603
10990
  });
10604
10991
  }
10992
+ /**
10993
+ * The ONE status-derivation function — used by the store and the kici-admin
10994
+ * host CLI. `ready` requires the host to be genuinely live (connected to some
10995
+ * instance AND heartbeat fresh — the freshness check catches a crashed
10996
+ * instance that never cleared `connected_instance_id`). It never returns
10997
+ * `ready` for a not-currently-live host: a not-live `static` reads
10998
+ * `unreachable` (the declared-but-absent alarm), a not-live `ephemeral` reads
10999
+ * `stale` (scaled down, awaiting reap).
11000
+ */
11001
+ function deriveHostStatus(row, nowMs, graceMs) {
11002
+ const ageMs = nowMs - new Date(row.last_seen).getTime();
11003
+ if (row.connected_instance_id !== null && ageMs <= graceMs) return "ready";
11004
+ if (row.lifecycle_class === "ephemeral") return "stale";
11005
+ return "unreachable";
11006
+ }
11007
+ /**
11008
+ * Durable, cluster-shared roster of every agent the cluster has ever enrolled.
11009
+ *
11010
+ * The in-memory `AgentRegistry` reconciles into this table on register
11011
+ * (`upsert`) / unregister (`markDisconnected`), with a coarse heartbeat
11012
+ * (`stampLastSeen`). All liveness writes are owner-guarded by `instanceId` so a
11013
+ * stale disconnect from an old instance can never clobber a row a different
11014
+ * instance now owns. The leader-only reaper deletes `ephemeral` rows past their
11015
+ * ttl via `reapEphemeralPastTtl`; `static` rows persist and read `unreachable`.
11016
+ */
11017
+ var HostRosterStore = class {
11018
+ db;
11019
+ constructor(db) {
11020
+ this.db = db;
11021
+ }
11022
+ /** Idempotent upsert on agent_id; stamps connected_instance_id + last_seen. */
11023
+ async upsert(input) {
11024
+ const labelsJson = JSON.stringify(input.labels);
11025
+ await this.db.insertInto("host_roster").values({
11026
+ agent_id: input.agentId,
11027
+ token_id: input.tokenId,
11028
+ lifecycle_class: input.lifecycleClass,
11029
+ labels: labelsJson,
11030
+ hostname: input.hostname,
11031
+ platform: input.platform,
11032
+ arch: input.arch,
11033
+ connected_instance_id: input.instanceId,
11034
+ last_seen: sql`now()`,
11035
+ updated_at: sql`now()`
11036
+ }).onConflict((oc) => oc.column("agent_id").doUpdateSet({
11037
+ token_id: input.tokenId,
11038
+ lifecycle_class: input.lifecycleClass,
11039
+ labels: labelsJson,
11040
+ hostname: input.hostname,
11041
+ platform: input.platform,
11042
+ arch: input.arch,
11043
+ connected_instance_id: input.instanceId,
11044
+ last_seen: sql`now()`,
11045
+ updated_at: sql`now()`
11046
+ })).execute();
11047
+ }
11048
+ /** Clear liveness on disconnect — but only if THIS instance still owns it. */
11049
+ async markDisconnected(agentId, instanceId) {
11050
+ await this.db.updateTable("host_roster").set({
11051
+ connected_instance_id: null,
11052
+ updated_at: sql`now()`
11053
+ }).where("agent_id", "=", agentId).where("connected_instance_id", "=", instanceId).execute();
11054
+ }
11055
+ /** Coarse heartbeat stamp — same owner-guard as markDisconnected. */
11056
+ async stampLastSeen(agentId, instanceId) {
11057
+ await this.db.updateTable("host_roster").set({ last_seen: sql`now()` }).where("agent_id", "=", agentId).where("connected_instance_id", "=", instanceId).execute();
11058
+ }
11059
+ /** Operator pre-declare of a static host before its agent dials in. */
11060
+ async declareStatic(input) {
11061
+ await this.db.insertInto("host_roster").values({
11062
+ agent_id: input.agentId,
11063
+ token_id: null,
11064
+ lifecycle_class: "static",
11065
+ labels: JSON.stringify(input.labels),
11066
+ hostname: input.hostname ?? null,
11067
+ connected_instance_id: null,
11068
+ last_seen: sql`now()`,
11069
+ updated_at: sql`now()`
11070
+ }).onConflict((oc) => oc.column("agent_id").doNothing()).execute();
11071
+ }
11072
+ async get(agentId) {
11073
+ return await this.db.selectFrom("host_roster").selectAll().where("agent_id", "=", agentId).executeTakeFirst() ?? null;
11074
+ }
11075
+ async listAll() {
11076
+ return this.db.selectFrom("host_roster").selectAll().orderBy("agent_id", "asc").execute();
11077
+ }
11078
+ /**
11079
+ * Resolve every roster host matching a `runsOnAll` predicate (OR-of-AND
11080
+ * include groups, minus exclude labels), tagged with its derived status. This
11081
+ * is the host-fanout resolver: it returns declared-but-absent static hosts
11082
+ * (status `unreachable`) so the caller can apply `onUnreachable` — the live
11083
+ * registry alone cannot name an expected-but-absent host.
11084
+ */
11085
+ async findMatching(include, exclude, graceMs) {
11086
+ const rows = await this.db.selectFrom("host_roster").selectAll().execute();
11087
+ const now = Date.now();
11088
+ const out = [];
11089
+ for (const row of rows) {
11090
+ const labels = JSON.parse(row.labels);
11091
+ const set = new Set(labels);
11092
+ if (exclude.some((e) => matcherSatisfiedBy(e, set))) continue;
11093
+ if (include.length && !include.some((grp) => grp.every((m) => matcherSatisfiedBy(m, set)))) continue;
11094
+ out.push({
11095
+ agentId: row.agent_id,
11096
+ host: row.hostname ?? row.agent_id,
11097
+ labels,
11098
+ lifecycleClass: row.lifecycle_class,
11099
+ connectedInstanceId: row.connected_instance_id,
11100
+ status: deriveHostStatus(row, now, graceMs),
11101
+ platform: row.platform,
11102
+ arch: row.arch
11103
+ });
11104
+ }
11105
+ out.sort((a, b) => a.agentId.localeCompare(b.agentId));
11106
+ return out;
11107
+ }
11108
+ /**
11109
+ * Count `static` (declared) hosts whose derived status is `unreachable` —
11110
+ * the "declared-but-absent" alarm population. Reuses the single-source
11111
+ * {@link deriveHostStatus} so the count never diverges from what
11112
+ * `kici-admin host list` shows. A not-currently-connected static host reads
11113
+ * `unreachable` regardless of grace (only the connected-but-stale case
11114
+ * depends on `graceMs`).
11115
+ */
11116
+ async countStaticUnreachable(graceMs) {
11117
+ const rows = await this.db.selectFrom("host_roster").selectAll().where("lifecycle_class", "=", "static").execute();
11118
+ const now = Date.now();
11119
+ return rows.filter((r) => deriveHostStatus(r, now, graceMs) === "unreachable").length;
11120
+ }
11121
+ /** Delete ephemeral rows whose last_seen is older than ttl. Returns count. */
11122
+ async reapEphemeralPastTtl(ttlMs) {
11123
+ const res = await this.db.deleteFrom("host_roster").where("lifecycle_class", "=", "ephemeral").where("last_seen", "<", sql`now() - (${ttlMs}::text || ' milliseconds')::interval`).executeTakeFirst();
11124
+ return Number(res.numDeletedRows ?? 0n);
11125
+ }
11126
+ };
11127
+ //#endregion
11128
+ //#region src/cli/commands/host.ts
11129
+ /**
11130
+ * The CLI reads the roster DB directly (no full orchestrator config load), so
11131
+ * it sources the grace window from the same env var the orchestrator config
11132
+ * defaults from. Default mirrors `config.ts` rosterGraceMs (300_000).
11133
+ */
11134
+ function rosterGraceMs() {
11135
+ const v = Number(process.env.KICI_ROSTER_GRACE_MS);
11136
+ return Number.isFinite(v) && v > 0 ? v : 3e5;
11137
+ }
11138
+ function formatHostTable(rows) {
11139
+ if (rows.length === 0) return "No hosts in the roster.";
11140
+ const header = "Agent ID | Class | Status | Instance | Last Seen | Labels";
11141
+ const sep = "-".repeat(57);
11142
+ const now = Date.now();
11143
+ const grace = rosterGraceMs();
11144
+ return [
11145
+ header,
11146
+ sep,
11147
+ ...rows.map((r) => {
11148
+ const status = deriveHostStatus(r, now, grace);
11149
+ const labels = JSON.parse(r.labels).join(",");
11150
+ return `${r.agent_id} | ${r.lifecycle_class} | ${status} | ${r.connected_instance_id ?? "-"} | ${new Date(r.last_seen).toISOString()} | ${labels}`;
11151
+ })
11152
+ ].join("\n");
11153
+ }
11154
+ function registerHostCommands(program) {
11155
+ const host = program.command("host").description("Inspect and declare the host roster");
11156
+ host.command("list").description("List all roster hosts").option("--json", "Output JSON").action(async (opts) => {
11157
+ try {
11158
+ const rows = await withDb((db) => new HostRosterStore(db).listAll());
11159
+ if (opts.json) {
11160
+ console.log(JSON.stringify(rows, null, 2));
11161
+ return;
11162
+ }
11163
+ console.log(formatHostTable(rows));
11164
+ } catch (err) {
11165
+ console.error(`Error: ${toErrorMessage(err)}`);
11166
+ process.exit(1);
11167
+ }
11168
+ });
11169
+ host.command("get").description("Show one roster host").requiredOption("--agent-id <id>", "Agent id").option("--json", "Output JSON").action(async (opts) => {
11170
+ try {
11171
+ const row = await withDb((db) => new HostRosterStore(db).get(opts.agentId));
11172
+ if (!row) {
11173
+ console.error(`No host found: ${opts.agentId}`);
11174
+ process.exit(1);
11175
+ }
11176
+ console.log(opts.json ? JSON.stringify(row, null, 2) : formatHostTable([row]));
11177
+ } catch (err) {
11178
+ console.error(`Error: ${toErrorMessage(err)}`);
11179
+ process.exit(1);
11180
+ }
11181
+ });
11182
+ host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").action(async (opts) => {
11183
+ try {
11184
+ const labels = opts.labels ? String(opts.labels).split(",").map((s) => s.trim()).filter(Boolean) : [];
11185
+ await withDb((db) => new HostRosterStore(db).declareStatic({
11186
+ agentId: opts.agentId,
11187
+ labels,
11188
+ hostname: opts.hostname
11189
+ }));
11190
+ console.log(`Declared static host: ${opts.agentId}`);
11191
+ } catch (err) {
11192
+ console.error(`Error: ${toErrorMessage(err)}`);
11193
+ process.exit(1);
11194
+ }
11195
+ });
11196
+ }
10605
11197
  //#endregion
10606
11198
  //#region src/cli/commands/org-settings.ts
10607
11199
  function formatSettings(s, format) {
@@ -14504,7 +15096,9 @@ function renderBootScript(cfg) {
14504
15096
  "#!/usr/bin/env bash",
14505
15097
  `# Generated by kici-admin firecracker --persist for bridge ${cfg.bridgeName}. Do not hand-edit.`,
14506
15098
  "set -euo pipefail",
14507
- `printf '%s\\n' "${NM_CONF_CONTENT.replace(/\n/g, "\\n").replace(/"/g, "\\\"")}" > ${NM_CONF_PATH} || true`,
15099
+ `cat > ${NM_CONF_PATH} <<'KICI_NM_EOF' || true`,
15100
+ NM_CONF_CONTENT.replace(/\n$/, ""),
15101
+ "KICI_NM_EOF",
14508
15102
  "HOST_IFACE=$(ip -j route show default | sed -n 's/.*\"dev\":\"\\([^\"]*\\)\".*/\\1/p' | head -1)",
14509
15103
  "if [ -z \"$HOST_IFACE\" ]; then echo \"no default-route iface\" >&2; exit 1; fi",
14510
15104
  ...lines,
@@ -17291,6 +17885,7 @@ function buildProgram() {
17291
17885
  registerScalerCommands(program);
17292
17886
  registerAgentServiceCommands(program);
17293
17887
  registerPeerCommands(program, getClient);
17888
+ registerHostCommands(program);
17294
17889
  registerOrgSettingsCommands(program, getClient);
17295
17890
  registerClusterNameCommands(program, getClient);
17296
17891
  registerMaintenanceCommands(program, getClient);