@songsid/agend 2.1.7-beta.8 → 2.1.8-beta.1

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/daemon.js CHANGED
@@ -979,6 +979,25 @@ export class Daemon extends EventEmitter {
979
979
  * after the startup scan can still swallow that message's Enter.
980
980
  */
981
981
  inputTransientGuardGeneration = null;
982
+ /**
983
+ * The spawn whose passive transients are over, independently of the
984
+ * startup-proof tail above. A transient screen can be quoted verbatim by the
985
+ * transcript (a cropped quote of the resume screen is byte-identical to the
986
+ * real one), so the pane alone must not be able to hold input. AgEnD's own
987
+ * launch state retires the check, per launch attempt: a launch that cannot show one, the real
988
+ * transient seen and then gone, or a "transient" that never changed for
989
+ * INPUT_TRANSIENT_STALL_MS. (The first submitted message retires the whole
990
+ * guard, as before.)
991
+ */
992
+ inputTransientRetiredAttempt = null;
993
+ /** The launch attempt in which a passive transient was actually observed on screen. */
994
+ inputTransientSeenAttempt = null;
995
+ /**
996
+ * One per command actually launched. A spawn can launch more than once
997
+ * (spawnClaudeWindow retries a failed resume without a new beginSpawn), and
998
+ * each launch paints its own transient, so retirement is per attempt.
999
+ */
1000
+ launchAttempt = 0;
982
1001
  skipResume = false;
983
1002
  startupAborted = false;
984
1003
  /** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
@@ -4945,19 +4964,39 @@ export class Daemon extends EventEmitter {
4945
4964
  if (this.inputTransientGuardGeneration === null
4946
4965
  || this.inputTransientGuardGeneration !== this.spawnGeneration)
4947
4966
  return [];
4967
+ if (this.inputTransientRetiredAttempt === this.launchAttempt)
4968
+ return [];
4948
4969
  return this.backend?.getInputUnavailableTransients?.() ?? [];
4949
4970
  }
4971
+ /** Stop honouring passive transients for the current spawn. */
4972
+ retireInputTransients(reason) {
4973
+ if (this.inputTransientRetiredAttempt === this.launchAttempt)
4974
+ return;
4975
+ this.inputTransientRetiredAttempt = this.launchAttempt;
4976
+ this.logger.debug({ reason, generation: this.spawnGeneration, attempt: this.launchAttempt }, "Startup input transient retired for this launch");
4977
+ }
4950
4978
  /** Match a passive transient against the current interactive screen. */
4951
4979
  inputTransientInPane(pane) {
4952
- for (const transient of this.guardedInputTransients()) {
4980
+ const transients = this.guardedInputTransients();
4981
+ if (transients.length === 0)
4982
+ return null;
4983
+ for (const transient of transients) {
4953
4984
  // `pattern` is only a cheap pre-filter. `isActive` is deliberately the
4954
4985
  // authority because users and agents routinely quote diagnostic text.
4955
4986
  transient.pattern.lastIndex = 0;
4956
4987
  if (!transient.pattern.test(pane))
4957
4988
  continue;
4958
- if (transient.isActive(pane))
4989
+ if (transient.isActive(pane)) {
4990
+ this.inputTransientSeenAttempt = this.launchAttempt;
4959
4991
  return transient;
4992
+ }
4960
4993
  }
4994
+ // The phase is over once it has been seen to end; from then on the same
4995
+ // screen is a quote. A settled-looking pane before that proves nothing:
4996
+ // codex 0.154 painted the prompt and footer first and "Resuming session…"
4997
+ // a moment later — the reason this guard outlives the startup scan.
4998
+ if (this.inputTransientSeenAttempt === this.launchAttempt)
4999
+ this.retireInputTransients("transient ended");
4961
5000
  return null;
4962
5001
  }
4963
5002
  /** Capture failure is unknown, never evidence that input is available. */
@@ -4993,7 +5032,12 @@ export class Daemon extends EventEmitter {
4993
5032
  return budget;
4994
5033
  }
4995
5034
  async probeInputTransient() {
4996
- if (this.guardedInputTransients().length === 0 || !this.tmux)
5035
+ // A retired transient still leaves the startup guard armed: an unreadable
5036
+ // pane keeps answering "unknown", so it stalls out on the same budget
5037
+ // instead of letting a delivery poll a dark pane for ever.
5038
+ const armed = this.inputTransientGuardGeneration !== null && this.inputTransientGuardGeneration === this.spawnGeneration
5039
+ && (this.backend?.getInputUnavailableTransients?.().length ?? 0) > 0;
5040
+ if (!armed || !this.tmux)
4997
5041
  return { state: "clear" };
4998
5042
  try {
4999
5043
  const pane = await this.tmux.capturePane();
@@ -5059,6 +5103,11 @@ export class Daemon extends EventEmitter {
5059
5103
  stalledForMs: budget.stalled ? INPUT_TRANSIENT_STALL_MS : undefined,
5060
5104
  ceilingMs: timeoutMs,
5061
5105
  }, "CLI input stayed unavailable — refusing to send Enter");
5106
+ // A screen that did not change for the whole stall window is not a
5107
+ // load in progress. This delivery still fails, but the same screen
5108
+ // (possibly a quote) must not fail every later one too.
5109
+ if (budget.stalled && observedDescription !== null)
5110
+ this.retireInputTransients("transient stalled");
5062
5111
  return false;
5063
5112
  }
5064
5113
  await new Promise(r => setTimeout(r, INPUT_TRANSIENT_POLL_MS));
@@ -5306,6 +5355,11 @@ export class Daemon extends EventEmitter {
5306
5355
  stalledForMs: transientBudget.stalled ? INPUT_TRANSIENT_STALL_MS : undefined,
5307
5356
  ceilingMs: INPUT_TRANSIENT_WAIT_MS,
5308
5357
  }, "CLI input stayed unavailable during the delivery-readiness wait");
5358
+ // Same rule as the pre-Enter wait: a screen frozen for the whole
5359
+ // stall window is not a load in progress, so it holds this delivery
5360
+ // only.
5361
+ if (transientBudget.stalled)
5362
+ this.retireInputTransients("transient stalled");
5309
5363
  return false;
5310
5364
  }
5311
5365
  await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
@@ -7570,6 +7624,13 @@ export class Daemon extends EventEmitter {
7570
7624
  }
7571
7625
  }
7572
7626
  const cmd = `${envPrefix} ` + this.backend.buildCommand(launchConfig);
7627
+ // Every launched command re-arms the passive-transient check, including a
7628
+ // retry inside the same spawn: its load is a new one.
7629
+ this.launchAttempt++;
7630
+ // A launch that cannot paint a passive transient (Codex: a fresh start
7631
+ // never shows "Resuming session…") must not honour one quoted on screen.
7632
+ if (this.backend.launchMayShowInputTransient?.() === false)
7633
+ this.retireInputTransients("launch shows none");
7573
7634
  // e.g. Codex could not read its session DB and chose a fallback (#984):
7574
7635
  // the operator should know which conversation this launch continues.
7575
7636
  const launchWarning = this.backend.consumeLaunchWarning?.() ?? null;