@cotal-ai/manager 0.106.1 → 0.107.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/manager.js CHANGED
@@ -376,6 +376,12 @@ export class Manager {
376
376
  /** See {@link ManagerOptions.installedExtensions}. */
377
377
  installedExtensions;
378
378
  executionState;
379
+ /** One backend per kind a seat has asked for, beside {@link runtime}. The manager's own runtime
380
+ * stays the default and is never replaced by a later seat's choice. */
381
+ runtimes = new Map();
382
+ /** Hosted pooled control, recorded so a later seat cannot add a custodial runtime the constructor
383
+ * already refused for the manager's own. */
384
+ pooled;
379
385
  /** Internal test seam. Production leaves this undefined and uses the scoped delivery-admin evictor. */
380
386
  staticLifecycleEvict;
381
387
  preserveStopTimeoutMs;
@@ -715,6 +721,7 @@ export class Manager {
715
721
  if (opts.secretStore)
716
722
  this.secretStoreIdentity = injectedSecretStoreIdentity(opts.secretStore);
717
723
  this.installedExtensions = opts.installedExtensions ?? false;
724
+ this.pooled = opts.pooled === true;
718
725
  this.executionState = opts.execution === "none" ? { kind: "none" } : { kind: "runtime" };
719
726
  if (!this.remoteAuthority) {
720
727
  const local = loadManagerInstanceIdentity(this.workspaceRoot, this.space);
@@ -772,6 +779,30 @@ export class Manager {
772
779
  throw new EpEnvelopeError("unimplemented", this.executionRefusal());
773
780
  return this.runtime;
774
781
  }
782
+ /** The backend one seat launches, restarts, and is reaped through. An omitted or blank choice is
783
+ * this manager's own runtime. A named one is created once, cached by its resolved kind, and reused
784
+ * for every later seat of that kind. The manager's own runtime is never replaced. An unknown or
785
+ * unreachable runtime throws, and pooled control refuses a custodial one by the same rule the
786
+ * constructor applied to the manager's own. */
787
+ runtimeFor(kind) {
788
+ if (this.executionState.kind === "none")
789
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
790
+ const asked = kind?.trim();
791
+ if (!asked)
792
+ return this.runtime;
793
+ if (asked === "auto")
794
+ throw new Error(`runtime "auto" chooses the manager's runtime, not one seat; omit runtime to use ${this.runtime.kind}`);
795
+ if (asked === this.runtime.kind)
796
+ return this.runtime;
797
+ const cached = this.runtimes.get(asked);
798
+ if (cached)
799
+ return cached;
800
+ const runtime = createRuntime(asked, `cotal-${this.space}`);
801
+ if (this.pooled && isCustodialRuntime(runtime))
802
+ throw new Error(`pooled control refuses custodial runtime "${runtime.kind}" before starting agents`);
803
+ this.runtimes.set(runtime.kind, runtime);
804
+ return runtime;
805
+ }
775
806
  /** What a default process stop does with this manager's seats. `release` drops manager-local
776
807
  * custody and leaves every seat running. `stop` means this runtime keeps its PTYs inside the
777
808
  * manager process, so those seats close with it: the stop stops and deprovisions them, and still
@@ -4546,10 +4577,11 @@ export class Manager {
4546
4577
  .reduce((max, p) => Math.max(max, p.ts), 0) + 1;
4547
4578
  // Same ordering as the spawn path: the replacement seat's reference is recorded on the
4548
4579
  // still-active slot before its processes exist, so a crash here never strands it.
4549
- const custody = this.reserveCustody();
4580
+ const runtime = this.runtimeFor(a.launch.runtime);
4581
+ const custody = this.reserveCustody(runtime);
4550
4582
  if (custody)
4551
4583
  await this.recordSlotCustody(a, custody);
4552
- const handle = await this.spawnCustodied(a.name, spec, a.launch.cwd, custody);
4584
+ const handle = await this.spawnCustodied(runtime, a.name, spec, a.launch.cwd, custody);
4553
4585
  replacement = handle;
4554
4586
  restart.sessionStatePath = spec.sessionStatePath ?? restart.sessionStatePath;
4555
4587
  if (continueSession !== undefined) {
@@ -4727,6 +4759,8 @@ export class Manager {
4727
4759
  // scalar/array (the CLI never does). Core doesn't interpret the keys; the connector validates them.
4728
4760
  if (args.launchOptions !== undefined && (typeof args.launchOptions !== "object" || args.launchOptions === null || Array.isArray(args.launchOptions)))
4729
4761
  return Promise.resolve({ ok: false, error: "launchOptions: expected a key:value mapping" });
4762
+ if (args.runtime !== undefined && (typeof args.runtime !== "string" || !args.runtime.trim()))
4763
+ return Promise.resolve({ ok: false, error: "runtime: expected a non-empty runtime name" });
4730
4764
  let supervise;
4731
4765
  if (args.supervise !== undefined) {
4732
4766
  const raw = args.supervise;
@@ -4815,6 +4849,7 @@ export class Manager {
4815
4849
  allowPublish,
4816
4850
  shareTools,
4817
4851
  ...(supervise !== undefined ? { supervise } : {}),
4852
+ ...(typeof args.runtime === "string" ? { runtime: args.runtime.trim() } : {}),
4818
4853
  route,
4819
4854
  }, caller, hooks, authenticatedCaller);
4820
4855
  }
@@ -5234,6 +5269,13 @@ export class Manager {
5234
5269
  // Before the first await, so the grant, the launch, the retained form and the restart slot all read
5235
5270
  // the lists and the restart policy as admitted.
5236
5271
  opts = ownLists(opts);
5272
+ let runtime;
5273
+ try {
5274
+ runtime = this.runtimeFor(opts.runtime);
5275
+ }
5276
+ catch (e) {
5277
+ return { ok: false, error: rejectionText(e) };
5278
+ }
5237
5279
  if (opts.resolved)
5238
5280
  opts.resolved = ownLists(opts.resolved);
5239
5281
  if (opts.supervise)
@@ -5312,8 +5354,8 @@ export class Manager {
5312
5354
  // exit, so a name cannot be respawned in place. User-mode seats have no static slot that
5313
5355
  // keeps the incarnation owned across a process death, so the same refusal applies there.
5314
5356
  if (opts.supervise !== undefined) {
5315
- if (this.requiredRuntime.kind !== "pty")
5316
- return { ok: false, error: `supervise is a restart policy this host cannot enforce: runtime "${this.requiredRuntime.kind}" cannot respawn a name in place` };
5357
+ if (runtime.kind !== "pty")
5358
+ return { ok: false, error: `supervise is a restart policy this host cannot enforce: runtime "${runtime.kind}" cannot respawn a name in place` };
5317
5359
  if (this.userMode)
5318
5360
  return { ok: false, error: "supervise is a restart policy this host cannot enforce: a user-mode seat has no static slot to keep the incarnation owned across a process death" };
5319
5361
  }
@@ -5330,8 +5372,8 @@ export class Manager {
5330
5372
  }
5331
5373
  // A delegated seat starts outside this host from values alone, so a choice only this host can
5332
5374
  // honour is refused here, before enrollment, and named rather than dropped.
5333
- if (this.requiredRuntime.spawnDelegated) {
5334
- const delegatedBy = `runtime "${this.requiredRuntime.kind}" starts seats outside this host`;
5375
+ if (runtime.spawnDelegated) {
5376
+ const delegatedBy = `runtime "${runtime.kind}" starts seats outside this host`;
5335
5377
  if (!this.remoteAuthority?.enrollManagedAgent)
5336
5378
  return { ok: false, error: `${delegatedBy}, which needs a host that enrolls managed agents (remoteAuthority.enrollManagedAgent); a delegated seat has no local grant path` };
5337
5379
  try {
@@ -5508,9 +5550,9 @@ export class Manager {
5508
5550
  }
5509
5551
  }
5510
5552
  // The child takes launch options as `--opt k=v`, so only string values can cross to it.
5511
- const unportable = this.requiredRuntime.spawnDelegated ? Object.keys(launchOptions ?? {}).filter((k) => typeof launchOptions[k] !== "string") : [];
5553
+ const unportable = runtime.spawnDelegated ? Object.keys(launchOptions ?? {}).filter((k) => typeof launchOptions[k] !== "string") : [];
5512
5554
  if (unportable.length)
5513
- return { ok: false, error: `runtime "${this.requiredRuntime.kind}" starts seats outside this host, so it cannot pass non-string launch options (${unportable.join(", ")})` };
5555
+ return { ok: false, error: `runtime "${runtime.kind}" starts seats outside this host, so it cannot pass non-string launch options (${unportable.join(", ")})` };
5514
5556
  // The AG-UI event plane is on unless the launch explicitly opted out. Refused HERE, before
5515
5557
  // anything is minted: a connector that cannot
5516
5558
  // emit must fail before provisioning rather than after, exactly as an unsupported `resume` does.
@@ -5599,7 +5641,7 @@ export class Manager {
5599
5641
  // launch. Minting it inside `runtime.spawn` (as it used to be) put the processes on disk
5600
5642
  // before anything recorded how to address them, so a crash in that window left a live seat
5601
5643
  // outside every manager and the successor retired the slot over it.
5602
- const custody = this.reserveCustody();
5644
+ const custody = this.reserveCustody(runtime);
5603
5645
  // ACCEPT SEAM (spawn-as-action): the incarnation identity is minted and NOTHING has
5604
5646
  // been provisioned yet — the action serve path binds the goal + replies the acceptance HERE. A
5605
5647
  // throw (bind conflict / duplicate goalId) aborts the spawn before provisioning: the catch below
@@ -5820,12 +5862,12 @@ export class Manager {
5820
5862
  // manager path is produced for it, only the handoff its runtime writes into the child.
5821
5863
  let spec;
5822
5864
  let handle;
5823
- if (this.requiredRuntime.spawnDelegated) {
5865
+ if (runtime.spawnDelegated) {
5824
5866
  const handoff = this.composeManagedHandoff(enrolled.material, enrolled.actorToken);
5825
5867
  if (this.delegatedLaunched.has(lifecycleUid))
5826
- throw new Error(`lifecycle ${lifecycleUid} was already handed to runtime "${this.requiredRuntime.kind}"; a lifecycle is handed off at most once`);
5868
+ throw new Error(`lifecycle ${lifecycleUid} was already handed to runtime "${runtime.kind}"; a lifecycle is handed off at most once`);
5827
5869
  this.delegatedLaunched.add(lifecycleUid);
5828
- handle = this.requiredRuntime.spawnDelegated({ agent, persona: readFileSync(configPath, "utf8"), managerInstanceId: this.managerInstanceId, role, model, variant, prompt, launchOptions: launchOptions, events }, handoff);
5870
+ handle = runtime.spawnDelegated({ agent, persona: readFileSync(configPath, "utf8"), managerInstanceId: this.managerInstanceId, role, model, variant, prompt, launchOptions: launchOptions, events }, handoff);
5829
5871
  }
5830
5872
  else {
5831
5873
  if (carried) {
@@ -5837,7 +5879,7 @@ export class Manager {
5837
5879
  if (carried)
5838
5880
  this.transcripts.consume(opts.resumeClaim, opts.resume);
5839
5881
  spec.env = { ...spec.env, COTAL_MANAGER_INSTANCE: this.managerInstanceId };
5840
- handle = await this.spawnCustodied(name, spec, cwd, custody);
5882
+ handle = await this.spawnCustodied(runtime, name, spec, cwd, custody);
5841
5883
  }
5842
5884
  hooks?.onLaunched?.(); // The "launched" progress edge (process spawned, pre-presence)
5843
5885
  const managed = {
@@ -5860,6 +5902,7 @@ export class Manager {
5860
5902
  control: spec?.control,
5861
5903
  ...(spec ? {} : { handedOff: true }),
5862
5904
  launch: {
5905
+ runtime: runtime.kind,
5863
5906
  source: opts.resolved
5864
5907
  ? {
5865
5908
  kind: "manifest",
@@ -6027,7 +6070,7 @@ export class Manager {
6027
6070
  // "no role", so dropping the key preserves the reply (surfaced by readiness:live).
6028
6071
  // A host that granted other channels than this spawn asked for: the receipt carries both.
6029
6072
  const acl = enrolled ? enrolledAcl({ allowSubscribe, allowPublish }, enrolled.material) : undefined;
6030
- const okData = { name, agent, id: managed.id, mode: handle.kind, streamsTerminal: this.streamsTerminal, lifecycleUid, ...(role !== undefined ? { role } : {}), ...(opts.eventsNotice !== undefined ? { eventsNotice: opts.eventsNotice } : {}), ...(acl ? { acl } : {}), ...(readiness.operator !== undefined ? { waitingFor: readiness.operator } : {}) };
6073
+ const okData = { name, agent, id: managed.id, mode: handle.kind, streamsTerminal: runtime.streamsTerminal, lifecycleUid, ...(role !== undefined ? { role } : {}), ...(opts.eventsNotice !== undefined ? { eventsNotice: opts.eventsNotice } : {}), ...(acl ? { acl } : {}), ...(readiness.operator !== undefined ? { waitingFor: readiness.operator } : {}) };
6031
6074
  await hooks?.onOutcome?.({ kind: "succeeded", data: okData });
6032
6075
  return { ok: true, data: okData };
6033
6076
  }
@@ -6371,7 +6414,8 @@ export class Manager {
6371
6414
  async orphanedResumeSeat(entry, principal, liveRoster) {
6372
6415
  if (!this.auth || this.userMode || entry.identity.mode !== "static")
6373
6416
  return undefined;
6374
- if (typeof this.requiredRuntime.reap !== "function")
6417
+ const runtime = this.runtimeFor(entry.launch.runtime);
6418
+ if (typeof runtime.reap !== "function")
6375
6419
  return undefined;
6376
6420
  if (liveRoster.some((p) => p.card.id === principal && p.lifecycleUid !== entry.identity.lifecycleUid))
6377
6421
  return undefined;
@@ -6380,14 +6424,14 @@ export class Manager {
6380
6424
  return undefined;
6381
6425
  if (ownedBySibling(slot, this.managerInstanceId))
6382
6426
  return undefined;
6383
- return slot.runtime?.kind === this.requiredRuntime.kind ? slot.runtime : undefined;
6427
+ return slot.runtime?.kind === runtime.kind ? slot.runtime : undefined;
6384
6428
  }
6385
6429
  /** Reap a seat {@link orphanedResumeSeat} found, then wait for its principal to leave presence so
6386
6430
  * the relaunch's readiness cannot match the dead seat's row. Returns the refusal when the reap
6387
6431
  * proves nothing or the principal stays live, which means another process holds it. */
6388
6432
  async reapOrphanedResumeSeat(principal, reference) {
6389
6433
  try {
6390
- const evidence = await requireRuntimeReap(this.requiredRuntime, reference);
6434
+ const evidence = await requireRuntimeReap(this.runtimeFor(reference.kind), reference);
6391
6435
  console.error(`resume: reaped orphaned seat ${reference.kind}:${reference.id} of retained principal "${principal}" (${evidence.detail})`);
6392
6436
  }
6393
6437
  catch (e) {
@@ -6413,8 +6457,8 @@ export class Manager {
6413
6457
  try {
6414
6458
  if (entry.space !== this.space)
6415
6459
  return { ok: false, error: `retained agent ${entry.name} belongs to space "${entry.space}", not "${this.space}"` };
6416
- if (entry.launch.runtime !== this.requiredRuntime.kind)
6417
- return { ok: false, error: `retained agent ${entry.name} requires runtime "${entry.launch.runtime}", current manager uses "${this.requiredRuntime.kind}"` };
6460
+ if (entry.launch.runtime !== this.runtimeFor(entry.launch.runtime).kind)
6461
+ return { ok: false, error: `retained agent ${entry.name} requires runtime "${entry.launch.runtime}", which this manager cannot resolve` };
6418
6462
  const nameErr = this.nameError(entry.name);
6419
6463
  if (nameErr)
6420
6464
  return { ok: false, error: nameErr };
@@ -6586,11 +6630,12 @@ export class Manager {
6586
6630
  if (!Number.isSafeInteger(readinessTimeoutMs) || readinessTimeoutMs <= 0)
6587
6631
  return { ok: false, error: `connector ${connector.name} declares invalid readinessTimeoutMs ${JSON.stringify(connector.readinessTimeoutMs)}; expected a positive safe integer` };
6588
6632
  const resumeId = entry.identity.mode === "user" ? principalKey(entry.identity.owner, entry.identity.actor).key : entry.identity.id;
6589
- const custody = this.reserveCustody();
6633
+ const runtime = this.runtimeFor(entry.launch.runtime);
6634
+ const custody = this.reserveCustody(runtime);
6590
6635
  if (custody)
6591
6636
  await this.recordSlotCustody({ name: entry.name, id: resumeId, lifecycleUid: entry.identity.lifecycleUid }, custody);
6592
6637
  prepared.spawned = true;
6593
- const handle = await this.spawnCustodied(entry.name, prepared.spec, entry.launch.cwd, custody);
6638
+ const handle = await this.spawnCustodied(runtime, entry.name, prepared.spec, entry.launch.cwd, custody);
6594
6639
  const managed = {
6595
6640
  name: entry.name,
6596
6641
  role: entry.role,
@@ -6615,6 +6660,7 @@ export class Manager {
6615
6660
  handle,
6616
6661
  control: prepared.spec.control,
6617
6662
  launch: {
6663
+ runtime: entry.launch.runtime,
6618
6664
  source: entry.launch.source,
6619
6665
  cwd: entry.launch.cwd,
6620
6666
  model: entry.launch.model,
@@ -9220,14 +9266,14 @@ export class Manager {
9220
9266
  /** Mint the custody reference for a seat this manager is about to spawn, BEFORE the processes
9221
9267
  * exist. Absent for a runtime with no durable custody (there is nothing a successor could reap
9222
9268
  * by reference), which is the only case that still spawns unreserved. */
9223
- reserveCustody() {
9224
- return isCustodialRuntime(this.requiredRuntime) ? this.requiredRuntime.reserve() : undefined;
9269
+ reserveCustody(runtime) {
9270
+ return isCustodialRuntime(runtime) ? runtime.reserve() : undefined;
9225
9271
  }
9226
9272
  /** Spawn under a reserved reference and prove the runtime honoured it. A runtime that minted its
9227
9273
  * own id instead would leave every durable record addressing a seat that does not exist, so the
9228
9274
  * mismatch tears the new seat down and throws rather than returning an unaddressable handle. */
9229
- async spawnCustodied(name, spec, cwd, reserved) {
9230
- const handle = this.requiredRuntime.spawn(name, spec, cwd, reserved);
9275
+ async spawnCustodied(runtime, name, spec, cwd, reserved) {
9276
+ const handle = runtime.spawn(name, spec, cwd, reserved);
9231
9277
  if (reserved === undefined)
9232
9278
  return handle;
9233
9279
  const got = handle.reference;
@@ -9249,7 +9295,7 @@ export class Manager {
9249
9295
  }
9250
9296
  else {
9251
9297
  try {
9252
- const evidence = await requireRuntimeReap(this.requiredRuntime, got);
9298
+ const evidence = await requireRuntimeReap(runtime, got);
9253
9299
  disposal = `the spawned seat was reaped (${evidence.detail})`;
9254
9300
  }
9255
9301
  catch (e) {
@@ -9263,7 +9309,7 @@ export class Manager {
9263
9309
  : `the spawned seat could NOT be reaped: ${rejectionText(e)}`;
9264
9310
  }
9265
9311
  }
9266
- throw new Error(`runtime "${this.requiredRuntime.kind}" reserved custody ${reserved.kind}:${reserved.id} for "${name}" but spawned ` +
9312
+ throw new Error(`runtime "${runtime.kind}" reserved custody ${reserved.kind}:${reserved.id} for "${name}" but spawned ` +
9267
9313
  `${got ? `${got.kind}:${got.id}` : "a handle with no reference"}; the recorded reference would address no seat, so ${disposal}`);
9268
9314
  }
9269
9315
  /** Persist what a successor needs to take this seat back if this process ends without stopping
@@ -9276,7 +9322,8 @@ export class Manager {
9276
9322
  if (!this.auth || this.userMode)
9277
9323
  return;
9278
9324
  const policy = a.restart?.policy;
9279
- const adoptable = a.handle.reference !== undefined && typeof this.requiredRuntime.adopt === "function";
9325
+ const runtime = this.runtimeFor(a.handle.kind);
9326
+ const adoptable = a.handle.reference !== undefined && typeof runtime.adopt === "function";
9280
9327
  if (!policy && !adoptable)
9281
9328
  return;
9282
9329
  const agent = this.resumeEntryOf(a);
@@ -9325,8 +9372,8 @@ export class Manager {
9325
9372
  return undefined;
9326
9373
  if (record.instanceId !== this.managerInstanceId || record.agent.space !== this.space)
9327
9374
  return undefined;
9328
- if (record.agent.launch.runtime !== this.requiredRuntime.kind) {
9329
- console.error(`! ${row.alias}: its seat ran on runtime "${record.agent.launch.runtime}" and this manager uses "${this.requiredRuntime.kind}"; the lifecycle is retired, not restarted`);
9375
+ if (record.agent.launch.runtime !== row.runtime?.kind) {
9376
+ console.error(`! ${row.alias}: its seat ran on runtime "${record.agent.launch.runtime}" and its slot names "${row.runtime?.kind ?? "none"}"; the lifecycle is retired, not restarted`);
9330
9377
  return undefined;
9331
9378
  }
9332
9379
  return record;
@@ -9341,7 +9388,7 @@ export class Manager {
9341
9388
  return "its record is not a static identity";
9342
9389
  if (row.runtime === undefined)
9343
9390
  return "its slot names no seat";
9344
- const runtime = this.requiredRuntime;
9391
+ const runtime = this.runtimeFor(row.runtime.kind);
9345
9392
  if (typeof runtime.adopt !== "function")
9346
9393
  return `runtime "${runtime.kind}" cannot adopt a seat`;
9347
9394
  // The credential is the manager's own lifecycle-keyed file, and its seed renews it.
@@ -9383,6 +9430,7 @@ export class Manager {
9383
9430
  // The runtime names an adopted handle by what it holds; the record names the seat.
9384
9431
  handle: { ...adopted, name: entry.name },
9385
9432
  launch: {
9433
+ runtime: entry.launch.runtime,
9386
9434
  source: entry.launch.source,
9387
9435
  cwd: entry.launch.cwd,
9388
9436
  model: entry.launch.model,
@@ -9425,7 +9473,7 @@ export class Manager {
9425
9473
  // names is reaped first. A runtime that cannot prove it gone refuses, and the terminal retires it.
9426
9474
  if (row.runtime !== undefined) {
9427
9475
  try {
9428
- const evidence = await requireRuntimeReap(this.requiredRuntime, row.runtime);
9476
+ const evidence = await requireRuntimeReap(this.runtimeFor(row.runtime.kind), row.runtime);
9429
9477
  console.error(`${row.alias}: the seat its previous manager left ${evidence.detail}`);
9430
9478
  }
9431
9479
  catch (error) {
@@ -9470,7 +9518,8 @@ export class Manager {
9470
9518
  // An unproven reap throws from here rather than printing, so it propagates into
9471
9519
  // driveStaticRetirement's catch, which records the failure and HOLDS the name. That is the whole
9472
9520
  // point of refusing: the alias must not be freed while a seat nobody proved gone may still run.
9473
- const evidence = await requireRuntimeReap(this.requiredRuntime, a.runtime);
9521
+ const runtime = this.runtimeFor(a.runtime.kind);
9522
+ const evidence = await requireRuntimeReap(runtime, a.runtime);
9474
9523
  console.error(`static retirement ${a.name}: orphan seat process ${evidence.detail}`);
9475
9524
  }
9476
9525
  /** The static F1 terminal for one departed incarnation (Unit B): delegates the gate/head CAS
@@ -10667,7 +10716,7 @@ export class Manager {
10667
10716
  // carried through verbatim, and the namespaced detail lets a caller act on the one fact
10668
10717
  // without reading the wording. This is the same answer `input` gives a runtime that cannot
10669
10718
  // write, for the same reason.
10670
- if (!this.streamsTerminal)
10719
+ if (!this.runtimeFor(a.handle.kind).streamsTerminal)
10671
10720
  throw new EpEnvelopeError("unimplemented", `the ${a.handle.kind} runtime streams no terminal, so agent "${a.name}" has none to attach: ${rejectionText(e)}`, [{ kind: RUNTIME_NO_TERMINAL_DETAIL, runtime: a.handle.kind }], "not-executed");
10672
10721
  throw new EpEnvelopeError("failed-precondition", rejectionText(e), undefined, "not-executed");
10673
10722
  }