@kici-dev/orchestrator 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -22056,7 +22056,7 @@ init_manifest();
22056
22056
  const ALL_PLATFORMS = AgentPlatform.options;
22057
22057
  /** The orchestrator's own version (single-version invariant) = the packaged agent version. */
22058
22058
  function resolveKiciVersion() {
22059
- return "0.7.0";
22059
+ return "0.8.0";
22060
22060
  }
22061
22061
  /** Parse the --platform value: default set | single | CSV | `all`. */
22062
22062
  function parsePlatforms(raw) {
@@ -32216,6 +32216,9 @@ var ContainerScalerBackend = class ContainerScalerBackend {
32216
32216
  state: "spawning"
32217
32217
  };
32218
32218
  this.agents.set(managed.id, managed);
32219
+ const assertStillTracked = () => {
32220
+ if (this.agents.get(managed.id) !== managed) throw new Error(`Agent ${agentId} was torn down while its container was being provisioned`);
32221
+ };
32219
32222
  try {
32220
32223
  const agentEnvForwarded = [];
32221
32224
  for (const [key, value] of Object.entries(process.env)) if (key.startsWith(KICI_AGENT_ENV_PREFIX) && value !== void 0) {
@@ -32289,17 +32292,24 @@ var ContainerScalerBackend = class ContainerScalerBackend {
32289
32292
  ...this.networkIsolation && { NetworkingConfig: { EndpointsConfig: { ["kici-agent-net"]: {} } } }
32290
32293
  });
32291
32294
  createdContainer = container;
32295
+ managed.backendRef = container.id;
32296
+ this.containerToManaged.set(container.id, managed.id);
32297
+ assertStillTracked();
32292
32298
  if (this.networkIsolation) emit(ScalerEventType.enum["scaler.network"], "configuring network isolation");
32293
32299
  await container.start({ abortSignal: signal });
32300
+ assertStillTracked();
32294
32301
  emit(ScalerEventType.enum["scaler.ready"], "container started");
32295
- if (this.networkIsolation) await this.applyIsolationRules({
32296
- agentId,
32297
- managedId: managed.id,
32298
- containerId: container.id,
32299
- networkPolicy: matchedLabelSet.networkPolicy,
32300
- orchestratorUrl,
32301
- ...signal ? { signal } : {}
32302
- });
32302
+ if (this.networkIsolation) {
32303
+ await this.applyIsolationRules({
32304
+ agentId,
32305
+ managedId: managed.id,
32306
+ containerId: container.id,
32307
+ networkPolicy: matchedLabelSet.networkPolicy,
32308
+ orchestratorUrl,
32309
+ ...signal ? { signal } : {}
32310
+ });
32311
+ assertStillTracked();
32312
+ }
32303
32313
  try {
32304
32314
  const capture = await createContainerLogCapture(this.docker, container.id);
32305
32315
  this.logCaptures.set(managed.id, capture);
@@ -32309,13 +32319,17 @@ var ContainerScalerBackend = class ContainerScalerBackend {
32309
32319
  error: toErrorMessage(err)
32310
32320
  });
32311
32321
  }
32322
+ assertStillTracked();
32312
32323
  managed.state = "running";
32313
- managed.backendRef = container.id;
32314
- this.containerToManaged.set(container.id, managed.id);
32315
32324
  emit(ScalerEventType.enum["agent.connecting"], "waiting for agent WS registration");
32316
32325
  return managed;
32317
32326
  } catch (err) {
32318
- const t = this.logCaptures.get(managed.id)?.tail() ?? "";
32327
+ const capture = this.logCaptures.get(managed.id);
32328
+ const t = capture?.tail() ?? "";
32329
+ if (capture) {
32330
+ capture.close();
32331
+ this.logCaptures.delete(managed.id);
32332
+ }
32319
32333
  const base = toErrorMessage(err);
32320
32334
  emit(ScalerEventType.enum["scaler.failed"], t ? `${base}\n--- captured output ---\n${t}` : base);
32321
32335
  const failedIp = this.containerIps.get(managed.id);
@@ -32325,10 +32339,13 @@ var ContainerScalerBackend = class ContainerScalerBackend {
32325
32339
  } catch {}
32326
32340
  this.containerIps.delete(managed.id);
32327
32341
  }
32328
- if (createdContainer) try {
32329
- await createdContainer.remove({ force: true });
32330
- } catch {}
32331
- this.agents.delete(managed.id);
32342
+ if (createdContainer) {
32343
+ this.containerToManaged.delete(createdContainer.id);
32344
+ try {
32345
+ await createdContainer.remove({ force: true });
32346
+ } catch {}
32347
+ }
32348
+ if (this.agents.get(managed.id) === managed) this.agents.delete(managed.id);
32332
32349
  throw err;
32333
32350
  }
32334
32351
  }
@@ -32365,6 +32382,7 @@ var ContainerScalerBackend = class ContainerScalerBackend {
32365
32382
  capture.close();
32366
32383
  this.logCaptures.delete(managedId);
32367
32384
  }
32385
+ if (!managed.backendRef) return;
32368
32386
  try {
32369
32387
  const container = this.docker.getContainer(managed.backendRef);
32370
32388
  try {
@@ -33025,7 +33043,7 @@ Token vocabulary:
33025
33043
  * where no bundler substituted the constant — the same shape `server.ts` and
33026
33044
  * `standalone.ts` use for `ORCHESTRATOR_VERSION`.
33027
33045
  */
33028
- const CLI_VERSION = "0.7.0";
33046
+ const CLI_VERSION = "0.8.0";
33029
33047
  /**
33030
33048
  * Build the kici-admin Commander program with every command group registered.
33031
33049
  * Exported so the surface registry can walk the real command tree without
@@ -15,6 +15,14 @@ import type { LockFileFetcher, LockFile } from '@kici-dev/engine';
15
15
  export declare class LocalLockFileFetcher implements LockFileFetcher {
16
16
  private readonly repoBasePath;
17
17
  readonly provider: 'local';
18
+ /**
19
+ * Never cached: `ref` is ignored below, so a cache keyed on it would serve the
20
+ * first lock file read to every later trigger for the lifetime of the entry —
21
+ * a `kici run --local` dispatch always triggers at `HEAD`, and an in-place run
22
+ * carries its uncommitted edits under an unchanged sha. Reading the file from
23
+ * disk is what makes those edits reach the run.
24
+ */
25
+ readonly cacheable: false;
18
26
  /**
19
27
  * @param repoBasePath - Base directory for the repo(s). When repoIdentifier
20
28
  * starts with 'file://', it is stripped and used as-is. Otherwise
@@ -34,8 +34,6 @@ export interface ClaimSpec {
34
34
  agentId: string;
35
35
  /** Exact label set the ephemeral token is authorized for. */
36
36
  labels: string[];
37
- /** Mandatory (taint) labels the pool gates on, carried for the workflow. */
38
- mandatoryLabels: string[];
39
37
  /** TTL of the ephemeral agent token minted on claim (seconds). */
40
38
  agentTokenTtlSeconds: number;
41
39
  /** Orchestrator WS URL the provisioned agent connects back to. */
@@ -108,12 +108,13 @@ export declare class EventScalerBackend implements ScalerBackend {
108
108
  */
109
109
  shutdownAll(): Promise<void>;
110
110
  /**
111
- * Apply the new config. The backend reads `roles`, `mandatoryLabels`,
112
- * `agentTokenTtlSeconds` and `provisioningTargets` off its entry at every
113
- * spawn, so the entry is replaced here otherwise a reload that retargets
114
- * the provisioning workflow, or changes the scaler's roles, would keep
115
- * emitting scale-up events to the old workflow refs and minting agent labels
116
- * from the old roles, while the manager's own routing gate used the new ones.
111
+ * Apply the new config. The backend reads `roles`, `agentTokenTtlSeconds`
112
+ * and `provisioningTargets` off its entry at every spawn (and
113
+ * `mandatoryLabels` only when a spawn reaches it without a context), so the
114
+ * entry is replaced here otherwise a reload that retargets the
115
+ * provisioning workflow, or changes the scaler's roles, would keep emitting
116
+ * scale-up events to the old workflow refs and minting agent labels from the
117
+ * old roles, while the manager's own routing gate used the new ones.
117
118
  */
118
119
  reload(labelSets: LabelSetConfig[], opts?: {
119
120
  maxAgents?: number;
@@ -1281,10 +1281,25 @@ export declare class ScalerManager {
1281
1281
  */
1282
1282
  getScalerContextForAgent(agentId: string): Record<string, unknown> | undefined;
1283
1283
  /**
1284
- * Start the warm pool idle check interval, the machine-pool ledger reaper,
1285
- * and the retirement sweep that tears down drained retiring backends.
1284
+ * Start the machine-pool ledger reaper and the retirement sweep that tears
1285
+ * down drained retiring backends, and publish the warm-pool gauges so each
1286
+ * pool's target is visible before its first fill.
1287
+ *
1288
+ * The warm pools themselves are NOT started here — see
1289
+ * {@link startWarmPools}, which the host calls once every dependency a fill
1290
+ * can reach is wired.
1286
1291
  */
1287
1292
  start(): void;
1293
+ /**
1294
+ * Run the warm pools' first deficit pass and arm their periodic tick.
1295
+ *
1296
+ * Separate from {@link start} because the first pass spawns immediately,
1297
+ * and on an event backend a spawn emits through the event emitter — which
1298
+ * the coordinator assigns after the scaler is built. The coordinator calls
1299
+ * this once the emitter exists; the worker, which hosts no event backend,
1300
+ * calls it right after `start()`.
1301
+ */
1302
+ startWarmPools(): void;
1288
1303
  /**
1289
1304
  * Provision/heal every backend's host prerequisites before spawning starts.
1290
1305
  * Awaits each backend's optional ensureHostReady, catching per-backend so one
@@ -1385,6 +1400,7 @@ export declare class ScalerManager {
1385
1400
  * coord boot or Raft leader switch. Reconstructs:
1386
1401
  *
1387
1402
  * - `spawningAgents` (with `boundJobId` preserved for eager-dispatch on register)
1403
+ * — event-backend rows only; a local-backend row is dropped, see below
1388
1404
  * - `agentJobCorrelation` (so scaler-lifecycle events route correctly)
1389
1405
  * - `reservations` + `perScalerUsage` (so the cap-check critical
1390
1406
  * section reflects the cluster-wide truth, not the local empty
@@ -374,6 +374,13 @@ export declare class ScalerStateStore {
374
374
  */
375
375
  export interface ScalerStateRecovery {
376
376
  spawningAgentsRehydrated: number;
377
+ /**
378
+ * Spawn rows released during recovery because their backend is local
379
+ * (container / firecracker / bare-metal): such a spawn cannot outlive the
380
+ * process that started it, so nothing could ever register against it, and
381
+ * rehydrating it would hold its reservation until the stale prune.
382
+ */
383
+ spawningDropped: number;
377
384
  agentJobsRehydrated: number;
378
385
  reservationsRehydrated: number;
379
386
  /**
@@ -97,6 +97,16 @@ export interface SpawnContext {
97
97
  * single source, and computing them twice is the defect this field closes.
98
98
  */
99
99
  platformTaints?: readonly string[];
100
+ /**
101
+ * The full taint gate the spawned agent is registered under: the pool's
102
+ * configured `mandatoryLabels` plus every derived platform taint, resolved
103
+ * by `ScalerManager` for the label set this spawn belongs to — the same set
104
+ * the durable spawn row carries. A backend publishes it (an event scaler puts
105
+ * it on the scale-up payload) so a provisioning workflow sees the gate the
106
+ * agent will actually be held to. A backend must never derive it itself: the
107
+ * manager is the single source, exactly as for `platformTaints`.
108
+ */
109
+ mandatoryLabels?: readonly string[];
100
110
  }
101
111
  /**
102
112
  * Network policy controlling RFC1918 and internet access for agents in this
@@ -125,10 +125,12 @@ export declare class WarmPoolManager {
125
125
  * Start the periodic tick: reap surplus agents past their idle timeout, then
126
126
  * top the pools back up.
127
127
  *
128
- * The first pass runs immediately rather than a tick later. Both hosts call
129
- * this only after `ensureHostsReady()`, so spawning here is safe — and
130
- * waiting would leave the pool empty for 30 seconds after every restart,
131
- * which is precisely the cold start it exists to remove.
128
+ * The first pass runs immediately rather than a tick later. The one caller
129
+ * is `ScalerManager.startWarmPools()`, which each host reaches only after
130
+ * `ensureHostsReady()` and on the coordinator after the event emitter
131
+ * is assigned, so spawning here is safe. Waiting a tick instead would leave
132
+ * the pool empty for 30 seconds after every restart, which is precisely the
133
+ * cold start it exists to remove.
132
134
  */
133
135
  start(): void;
134
136
  /**
@@ -40,7 +40,7 @@ export interface IdentityLink {
40
40
  * the answer is unambiguous; when they disagree there is no way to tell here
41
41
  * which provider serves the commit, so this answers undefined rather than
42
42
  * naming the wrong account. Undefined is a safe answer at every call site: the
43
- * attribution is simply omitted, never replaced by the raw subject id.
43
+ * attribution is omitted, never replaced by the raw subject id.
44
44
  */
45
45
  export declare function resolveLinkedUsername(identityLinks: IdentityLink[], userId: string): string | undefined;
46
46
  /**
@@ -64,7 +64,7 @@ import type { Database } from '../db/types.js';
64
64
  * What it does not pin: the field *set*. A fourth directory field added to
65
65
  * `trustPolicyUpdateSchema` is not picked up here, and `server.ts` hand-builds
66
66
  * the value it passes to `upsertFromPlatform`, so nothing would fail — the new
67
- * field would simply go unpersisted. Adding one means extending this object and
67
+ * field would go unpersisted. Adding one means extending this object and
68
68
  * that literal together.
69
69
  */
70
70
  export declare const trustDirectorySchema: z.ZodObject<{