@cotal-ai/manager 0.101.2 → 0.101.4

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
@@ -64,6 +64,10 @@ export const READINESS_TIMEOUT_MS = 30_000;
64
64
  * how long it waits past the window for the proof of its last read. An operator waits on this reply,
65
65
  * so it asks more often than the exit watch that outlives readiness. */
66
66
  const READINESS_EXIT_POLL_MS = 1_000;
67
+ /** How long the process of a launch that waits for the operator ({@link Connector.waitsForOperator})
68
+ * must run without exiting before it is ready: long enough that a harness refusing its arguments
69
+ * exits first and is reported as failed. */
70
+ const OPERATOR_READINESS_MS = 3_000;
67
71
  /** Managed same-session crash recovery follows the Codex host precedent: three restarts are allowed
68
72
  * inside a rolling two-minute window; the fourth crash is a loop and retires the seat loud. */
69
73
  const SESSION_RESTART_LIMIT = 3;
@@ -3732,7 +3736,7 @@ export class Manager {
3732
3736
  const refusal = material === undefined && !unshredded.length ? caughtEnvelope(e) : undefined;
3733
3737
  return {
3734
3738
  error: `agent auth preflight failed for "${name}": ${rejectionText(e)}${leftover}`,
3735
- ...(refusal?.outcome === "not-executed" ? { refused: refusal.code } : {}),
3739
+ ...(refusal?.outcome === "not-executed" ? { refused: refusal.code, ...(refusal.details ? { details: refusal.details } : {}) } : {}),
3736
3740
  };
3737
3741
  }
3738
3742
  }
@@ -5516,7 +5520,7 @@ export class Manager {
5516
5520
  // manager's agent) refuses cleanly at accept, rather than minting the collision and black-
5517
5521
  // holing on the broker/auth refusal (a pinned name live under another manager MUST refuse).
5518
5522
  if (this.nameInUse(identityName))
5519
- return { ok: false, error: `the name "${identityName}" is hard-pinned (${opts.resolved ? "manifest-declared" : "--name/identity override"}) but is already held by a live incarnation (managed here, an unmanaged foreground/connector session, or another manager's agent); a pinned same-name collision refuses at accept - pick another name or despawn the existing one` };
5523
+ return { ok: false, code: "already-exists", error: `the name "${identityName}" is hard-pinned (${opts.resolved ? "manifest-declared" : "--name/identity override"}) but is already held by a live incarnation (managed here, an unmanaged foreground/connector session, or another manager's agent); a pinned same-name collision refuses at accept - pick another name or despawn the existing one` };
5520
5524
  name = identityName;
5521
5525
  }
5522
5526
  else {
@@ -5657,7 +5661,7 @@ export class Manager {
5657
5661
  if ("error" in prep) {
5658
5662
  if (prep.refused !== undefined)
5659
5663
  hooks?.onUndone?.(prep.refused);
5660
- return { ok: false, error: prep.error };
5664
+ return { ok: false, error: prep.error, ...(prep.details ? { details: prep.details } : {}) };
5661
5665
  }
5662
5666
  // THE HOST'S UID WINS (#1972). On the local arm this is the value passed in, unchanged. On
5663
5667
  // the hosted enrollment arm the host selected it, and from here every lifecycle-keyed thing
@@ -5891,8 +5895,10 @@ export class Manager {
5891
5895
  // #159 B1: reply on a REAL outcome, not a timer. Wait for the agent to actually join the mesh
5892
5896
  // (presence) → started, the child to exit → failed (with its last output; already reaped), or
5893
5897
  // neither in time → uncertain. `✓ started` therefore means "it joined", never just "a process
5894
- // launched".
5895
- const readiness = await this.awaitReadiness(managed, readinessTimeoutMs);
5898
+ // launched", except for a launch whose options make the harness wait for the operator first: that
5899
+ // one is ready once its process runs, and its reply names the screen it waits at.
5900
+ const operator = connector.waitsForOperator?.(launchOptions);
5901
+ const readiness = await this.awaitReadiness(managed, readinessTimeoutMs, operator === undefined ? {} : { operator });
5896
5902
  // Deliberately stopped mid-launch: reaped by onExit, and the despawn/stop path owns the
5897
5903
  // goal terminal. Return BEFORE the failed/uncertain arms so this emits no competing
5898
5904
  // outcome and does not re-arm an exit watcher on an agent already gone.
@@ -5915,7 +5921,7 @@ export class Manager {
5915
5921
  this.watchExit(managed);
5916
5922
  if (managed.launch.carried)
5917
5923
  void this.refuseLateCarriedFork(managed, readinessTimeoutMs);
5918
- await hooks?.onOutcome?.({ kind: "uncertain", data: { reason: readiness.detail } });
5924
+ await hooks?.onOutcome?.({ kind: "uncertain", data: { reason: readiness.detail, ...(readiness.running ? { running: true } : {}) } });
5919
5925
  return { ok: false, error: readiness.detail };
5920
5926
  }
5921
5927
  if (managed.launch.carried) {
@@ -5987,7 +5993,7 @@ export class Manager {
5987
5993
  // "no role", so dropping the key preserves the reply (surfaced by readiness:live).
5988
5994
  // A host that granted other channels than this spawn asked for: the receipt carries both.
5989
5995
  const acl = enrolled ? enrolledAcl({ allowSubscribe, allowPublish }, enrolled.material) : undefined;
5990
- const okData = { name, agent, id: managed.id, mode: handle.kind, lifecycleUid, ...(role !== undefined ? { role } : {}), ...(opts.eventsNotice !== undefined ? { eventsNotice: opts.eventsNotice } : {}), ...(acl ? { acl } : {}) };
5996
+ const okData = { name, agent, id: managed.id, mode: handle.kind, lifecycleUid, ...(role !== undefined ? { role } : {}), ...(opts.eventsNotice !== undefined ? { eventsNotice: opts.eventsNotice } : {}), ...(acl ? { acl } : {}), ...(readiness.operator !== undefined ? { waitingFor: readiness.operator } : {}) };
5991
5997
  await hooks?.onOutcome?.({ kind: "succeeded", data: okData });
5992
5998
  return { ok: true, data: okData };
5993
5999
  }
@@ -6713,7 +6719,10 @@ export class Manager {
6713
6719
  * catches a join/exit that landed before we subscribed). A runtime whose `attach()` throws (an external
6714
6720
  * surface) streams no output or exit, so its exit leg polls the runtime's exit proof ({@link pollProvedExit}),
6715
6721
  * reads it once more when the window closes, and its failure carries no last output; one that cannot prove an
6716
- * exit either races presence-vs-backstop only. */
6722
+ * exit either races presence-vs-backstop only.
6723
+ * A launch that waits for the operator at `opts.operator`, a screen its connector names, is ready once a
6724
+ * process this runtime owns has run for {@link OPERATOR_READINESS_MS} without exiting. An uncertain verdict
6725
+ * says whether a process this runtime owns is still running. */
6717
6726
  async awaitReadiness(a, readinessTimeoutMs, opts = {}) {
6718
6727
  let session;
6719
6728
  try {
@@ -6748,11 +6757,13 @@ export class Manager {
6748
6757
  let timer;
6749
6758
  let unsubExit = () => { };
6750
6759
  let joinedPoll;
6760
+ let operatorTimer;
6751
6761
  const finish = (r) => {
6752
6762
  if (done)
6753
6763
  return;
6754
6764
  done = true;
6755
6765
  clearTimeout(timer);
6766
+ clearTimeout(operatorTimer);
6756
6767
  if (joinedPoll !== undefined)
6757
6768
  clearInterval(joinedPoll);
6758
6769
  this.ep.off("presence", onPresence);
@@ -6775,6 +6786,7 @@ export class Manager {
6775
6786
  if (done)
6776
6787
  return;
6777
6788
  clearTimeout(timer);
6789
+ clearTimeout(operatorTimer);
6778
6790
  // `stopHandle` sets `terminalizing` synchronously before it kills, and `onAgentExit` sets it
6779
6791
  // through `freeSlot` when it retires the seat, so only a read before the await and the reap
6780
6792
  // tells a deliberate stop from a launch failure.
@@ -6822,11 +6834,21 @@ export class Manager {
6822
6834
  finish({ ok: false, detail: `${a.name} exited on launch${tail ? ` - last output: ${tail}` : ""}${exitDetail}` });
6823
6835
  })();
6824
6836
  };
6825
- const uncertain = () => finish({
6826
- ok: false,
6827
- uncertain: true,
6828
- detail: `${a.name} (${a.id}): launch status uncertain - no process exit and no mesh presence within ${readinessTimeoutMs}ms; it may still be booting or stuck before connector startup. Inspect with \`cotal attach ${a.name}\` / \`cotal ps\`; do not stop it solely because this bounded wait elapsed.`,
6829
- });
6837
+ // A process this runtime owns reports its own exit, so one still running here is running.
6838
+ const uncertain = () => {
6839
+ const running = s !== undefined && a.handle.status() === "running";
6840
+ finish({
6841
+ ok: false,
6842
+ uncertain: true,
6843
+ running,
6844
+ detail: running
6845
+ ? `${a.name} (${a.id}): launch status uncertain - its process is running but did not join the mesh within ${readinessTimeoutMs}ms; it may still be starting or waiting at a prompt. Inspect with \`cotal attach ${a.name}\` / \`cotal ps\`; do not stop it solely because this bounded wait elapsed.`
6846
+ : `${a.name} (${a.id}): launch status uncertain - no process exit and no mesh presence within ${readinessTimeoutMs}ms; it may still be booting or stuck before connector startup. Inspect with \`cotal attach ${a.name}\` / \`cotal ps\`; do not stop it solely because this bounded wait elapsed.`,
6847
+ });
6848
+ };
6849
+ if (opts.operator !== undefined && s)
6850
+ operatorTimer = setTimeout(() => { if (a.handle.status() === "running")
6851
+ finish({ ok: true, operator: opts.operator }); }, OPERATOR_READINESS_MS);
6830
6852
  // The exit poll read the runtime up to a poll ago, and a window can be shorter than a poll, so
6831
6853
  // the window closes on a fresh read: a child that exited by then is reported as exited. Nothing
6832
6854
  // bounds how long a runtime's proof takes, so it gets one more poll before the launch is uncertain.
@@ -8309,8 +8331,8 @@ export class Manager {
8309
8331
  else {
8310
8332
  // Forward the readiness detail as the terminal's reason: this manager owns the deadline,
8311
8333
  // so it owns what elapsing it MEANS. Absent, core commits its own generic line (#605).
8312
- const why = o.data?.reason;
8313
- ({ fact } = await settleGoalUncertain(gw.ctx, { ref, now: Date.now(), committer: { instanceId: this.managerInstanceId, epoch }, ...(typeof why === "string" && why.length > 0 ? { reason: why } : {}) }));
8334
+ const { reason: why, running } = (o.data ?? {});
8335
+ ({ fact } = await settleGoalUncertain(gw.ctx, { ref, now: Date.now(), committer: { instanceId: this.managerInstanceId, epoch }, ...(typeof why === "string" && why.length > 0 ? { reason: why } : {}), ...(running === true ? { running: true } : {}) }));
8314
8336
  }
8315
8337
  this.expireGoalAcceptance(ref);
8316
8338
  this.emitGoalProgress(ref, epoch, { phase: "terminal", state: fact.state, ...(fact.data !== undefined ? { data: fact.data } : {}) });
@@ -8407,7 +8429,9 @@ export class Manager {
8407
8429
  // outcome unknown.
8408
8430
  if (acceptance === undefined) {
8409
8431
  const details = reply.details;
8410
- rejectAccept(new EpEnvelopeError(undone ?? "failed-precondition", reply.error ?? "spawn refused at accept", details, effectStarted && undone === undefined ? undefined : "not-executed"));
8432
+ // A hard-pinned name a live incarnation holds is the one refusal here that names its own code.
8433
+ const code = undone ?? (reply.code === "already-exists" ? "already-exists" : "failed-precondition");
8434
+ rejectAccept(new EpEnvelopeError(code, reply.error ?? "spawn refused at accept", details, effectStarted && undone === undefined ? undefined : "not-executed"));
8411
8435
  return;
8412
8436
  }
8413
8437
  // H1: `run` CATCHES its own body (a throw in buildLaunch/runtime.spawn) and RESOLVES `{ok:false}`