@songsid/agend 2.3.4-beta.4 → 2.3.4-beta.6

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
@@ -9622,29 +9622,28 @@ export class Daemon extends EventEmitter {
9622
9622
  }
9623
9623
  /** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
9624
9624
  async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
9625
- // A recovery Enter may have waited for a transient while cancel or spawn
9626
- // replaced the delivery. Check at the last point before the tmux write.
9625
+ // A recovery Enter may have waited for a transient while cancel or spawn replaced the delivery: check here, and
9626
+ // again after every wait below, at the last point before the tmux write.
9627
+ // M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
9628
+ // tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
9629
+ // the CLI this paste went to is never sent to its successor.
9627
9630
  const tmux = this.tmux;
9628
- const backend = this.backend;
9629
9631
  const generation = this.spawnGeneration;
9630
9632
  const fence = this.launchFenceEpoch;
9631
- const windowId = tmux?.getWindowId();
9632
- const current = () => !!tmux && !!windowId && this.tmux === tmux && this.backend === backend
9633
- && this.spawnGeneration === generation && this.launchFenceEpoch === fence && tmux.getWindowId() === windowId
9633
+ const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
9634
9634
  && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
9635
9635
  if (!current())
9636
9636
  return false;
9637
- // A paste-settle wait or retry can outlive the composer it saw. Passive startup-transient checks do not prove
9638
- // that a permission card has not replaced it. Read this exact pane again, without resolving any dialog.
9637
+ // The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
9639
9638
  const hold = () => {
9640
- onInputHeld?.();
9641
- this.logger.warn({ phase }, "Delivery input is not a verified composer; Enter withheld");
9639
+ onInputHeld?.("pane-in-mode");
9640
+ this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
9642
9641
  return false;
9643
9642
  };
9644
- // M-2267 (#2971 review, Prism P2): a pane in a tmux mode would take this Enter. Wait for the mode to end FIRST, then
9645
- // prove the composer (and that no dialog took its place) on a capture taken after it — and again whenever the pane
9646
- // went back into a mode meanwhile: the Enter only ever goes on a proof newer than the last mode. Never ended for the
9647
- // person, never answering a dialog that appeared during the wait (hold() then, as for any).
9643
+ // M-2267 (#2971): a pane in a tmux mode (a person left it in copy-mode) would take this Enter. Wait for the mode to
9644
+ // end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
9645
+ // past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
9646
+ // f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
9648
9647
  const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9649
9648
  for (;;) {
9650
9649
  const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
@@ -9652,50 +9651,6 @@ export class Daemon extends EventEmitter {
9652
9651
  return false;
9653
9652
  if (mode === "held")
9654
9653
  return hold();
9655
- let pane;
9656
- try {
9657
- pane = await tmux.capturePane();
9658
- }
9659
- catch {
9660
- return current() ? hold() : false;
9661
- }
9662
- if (!current())
9663
- return false;
9664
- let composer = false;
9665
- try {
9666
- if (this.runtimeDialogs().some(dialog => Daemon.dialogMatches(dialog, pane)))
9667
- return hold();
9668
- if (backend?.readInputRow)
9669
- composer = backend.readInputRow(pane) != null;
9670
- else if (backend?.inputDraft)
9671
- composer = backend.inputDraft(pane) != null;
9672
- else if (backend?.isDeliveryInputReadyPane)
9673
- composer = this.deliveryInputReadyPane(pane);
9674
- else if (backend?.readSteerComposer?.(pane) != null)
9675
- composer = true;
9676
- else if (backend) {
9677
- // Legacy backends still need their own current ready chrome, never a menu's cursor or old transcript row.
9678
- const shape = unrecognisedPromptShape(pane);
9679
- const ownComposer = backend.isDeliveryComposerPane?.(pane) === true;
9680
- if ((!selectionCard(pane, "CLI") || ownComposer) && (!shape || ownComposer || this.cliComposerShowing(pane, shape))) {
9681
- const bottom = backend.getBottomReadyPattern?.();
9682
- const ready = backend.getReadyPattern();
9683
- ready.lastIndex = 0;
9684
- composer = ownComposer || (bottom ? bottomRowIsReady(pane, bottom)
9685
- : ready.test(pane.trimEnd().split("\n").slice(-COMPOSER_TAIL_ROWS).join("\n")));
9686
- }
9687
- }
9688
- }
9689
- catch {
9690
- return hold();
9691
- }
9692
- if (!composer)
9693
- return hold();
9694
- if (!current())
9695
- return false;
9696
- // The proof stands only if the pane did not go into a mode while it was made: else wait, and prove again — within
9697
- // the same deadline (#2971 re-review, Prism P2: a pane flickering in and out of a mode must not hold the write lock
9698
- // past it), and never spinning: one poll interval before the next look.
9699
9654
  if (await this.paneInTmuxMode(phase)) {
9700
9655
  if (!current())
9701
9656
  return false;
@@ -9710,6 +9665,37 @@ export class Daemon extends EventEmitter {
9710
9665
  return false;
9711
9666
  break;
9712
9667
  }
9668
+ // M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
9669
+ // (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
9670
+ // would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
9671
+ // delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
9672
+ // — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
9673
+ // Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
9674
+ // that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
9675
+ // unrecognised-prompt guess — so an unknown screen still sends.
9676
+ if (onInputHeld) {
9677
+ let finalPane = null;
9678
+ try {
9679
+ finalPane = await tmux.capturePane();
9680
+ }
9681
+ catch {
9682
+ finalPane = null;
9683
+ }
9684
+ if (!current())
9685
+ return false;
9686
+ let dialog;
9687
+ try {
9688
+ dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
9689
+ }
9690
+ catch {
9691
+ dialog = undefined;
9692
+ }
9693
+ if (dialog !== undefined) {
9694
+ onInputHeld?.("dialog");
9695
+ this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
9696
+ return false;
9697
+ }
9698
+ }
9713
9699
  // W1: persist the "Enter may have happened" boundary before tmux receives
9714
9700
  // the key. A crash after this commit is never auto-replayed without a
9715
9701
  // positive transcript marker, even if the stale pane still shows composer.
@@ -9981,22 +9967,23 @@ export class Daemon extends EventEmitter {
9981
9967
  let enterAt = Date.now();
9982
9968
  const enterMono = performance.now();
9983
9969
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
9984
- let inputHeld = false;
9985
- if (!(await this.sendDeliveryEnter("initial-submit", steerEnterCurrent, durableAttempt, () => { inputHeld = true; }))) {
9970
+ let inputHeld = null;
9971
+ if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
9986
9972
  if (inputHeld) {
9987
- // The paste may still be in the CLI. Keep the existing uncertain path; no false failure or blind replay.
9973
+ // #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
9974
+ // the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
9988
9975
  verdict.phase = "submit-enter";
9989
- verdict.proof = "input-not-verified";
9976
+ verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
9990
9977
  return false;
9991
9978
  }
9992
- if (!steerCurrent()) {
9979
+ if (steerComposerProof && !steerCurrent()) {
9993
9980
  verdict.phase = "submit-enter";
9994
9981
  verdict.proof = "fenced-before-enter";
9995
9982
  return false;
9996
9983
  }
9997
9984
  if (steerComposerProof && performance.now() > steerEnterDeadline)
9998
9985
  return this.failDelivery(verdict, status, "submit-enter", "pre-enter-deadline");
9999
- return this.failDelivery(verdict, status, "submit-enter", "enter-not-sent");
9986
+ return this.failDelivery(verdict, status, "submit-enter", "tmux-send-keys-failed");
10000
9987
  }
10001
9988
  // The proof's whole budget, from the Enter: every capture and dialog probe inside it, a late answer dropped.
10002
9989
  const steerProofDeadline = performance.now() + NATIVE_QUEUE_PASTE_VERIFY_MS + POST_ENTER_PROOF_WINDOW_MS;
@@ -10018,7 +10005,7 @@ export class Daemon extends EventEmitter {
10018
10005
  if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
10019
10006
  await new Promise(r => setTimeout(r, 1_000));
10020
10007
  const retryAt = Date.now();
10021
- if (await this.sendDeliveryEnter("queue-less-defensive-retry", steerCurrent)) {
10008
+ if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
10022
10009
  enterAt = retryAt;
10023
10010
  enterRetry = true;
10024
10011
  this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
@@ -10149,7 +10136,7 @@ export class Daemon extends EventEmitter {
10149
10136
  // again. The prompt is back now, so an Enter can land.
10150
10137
  this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
10151
10138
  const strandedAt = Date.now();
10152
- if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", steerCurrent))) {
10139
+ if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
10153
10140
  return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
10154
10141
  }
10155
10142
  // The recovery Enter can be accepted before Codex paints its new
@@ -10202,7 +10189,7 @@ export class Daemon extends EventEmitter {
10202
10189
  }
10203
10190
  await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
10204
10191
  const retryAt = Date.now();
10205
- if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", steerCurrent))) {
10192
+ if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
10206
10193
  return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
10207
10194
  }
10208
10195
  if (windowId && this.controlClient) {
@@ -10254,7 +10241,7 @@ export class Daemon extends EventEmitter {
10254
10241
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
10255
10242
  const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
10256
10243
  const retryAt = Date.now();
10257
- if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", steerCurrent))) {
10244
+ if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
10258
10245
  return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
10259
10246
  }
10260
10247
  submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
@@ -10342,7 +10329,7 @@ export class Daemon extends EventEmitter {
10342
10329
  }
10343
10330
  // No control client to observe output: fall back to the legacy double-Enter.
10344
10331
  await new Promise(r => setTimeout(r, 1000));
10345
- await this.sendDeliveryEnter("unobserved-defensive-retry", steerCurrent);
10332
+ await this.sendDeliveryEnter("unobserved-defensive-retry");
10346
10333
  if (status)
10347
10334
  this.emit("message_confirmed", status); // ✅ (best-effort)
10348
10335
  }
@@ -10810,27 +10797,20 @@ export class Daemon extends EventEmitter {
10810
10797
  * `guard` is for a paste that must not outlive the reason it was made (the continue nudge): `accept` judges the ONE
10811
10798
  * capture taken here — the same picture is the baseline, the expected-screen check and the idle check — and `current`
10812
10799
  * is asked again after every await that precedes a write, so a cancelled paste adds no further paste and no key
10813
- * (the retries included). Every system paste also keeps the pane owner from before its first await.
10800
+ * (the retries included). Without a guard it is exactly the old unconditional path.
10814
10801
  */
10815
10802
  async submitSystemPaste(text, label, guard, admission) {
10816
- const tmux = this.tmux, generation = this.spawnGeneration, fence = this.launchFenceEpoch;
10817
- const windowId = tmux?.getWindowId();
10818
- const current = () => {
10819
- admission?.();
10820
- return !!tmux && !!windowId && this.tmux === tmux && tmux.getWindowId() === windowId
10821
- && this.spawnGeneration === generation && this.launchFenceEpoch === fence
10822
- && !this.deliveryWritesStopping && (guard?.current() ?? true);
10823
- };
10824
- if (!current())
10803
+ const current = guard || admission ? () => { admission?.(); return guard?.current() ?? true; } : undefined;
10804
+ if (current && !current())
10825
10805
  return false;
10826
- if (!tmux)
10806
+ if (!this.tmux)
10827
10807
  return false;
10828
10808
  const signature = this.submissionSignature(text);
10829
10809
  let baseline;
10830
10810
  if (guard) {
10831
10811
  let pane;
10832
10812
  try {
10833
- pane = await tmux.capturePane();
10813
+ pane = await this.tmux.capturePane();
10834
10814
  }
10835
10815
  catch {
10836
10816
  return false;
@@ -10845,7 +10825,7 @@ export class Daemon extends EventEmitter {
10845
10825
  if (current && !current())
10846
10826
  return false;
10847
10827
  const pasteGeneration = this.spawnGeneration;
10848
- const pasted = current ? await tmux.pasteBuffer(text, { guard: current }) : await tmux.pasteBuffer(text);
10828
+ const pasted = current ? await this.tmux.pasteBuffer(text, { guard: current }) : await this.tmux.pasteBuffer(text);
10849
10829
  if (!pasted) {
10850
10830
  this.logger.warn({ label }, "System paste failed to reach the pane");
10851
10831
  return false;