@songsid/agend 2.3.4-beta.2 → 2.3.4-beta.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.
Files changed (100) hide show
  1. package/dist/auth-api.js +3 -0
  2. package/dist/auth-api.js.map +1 -1
  3. package/dist/backend/claude-code.js +1 -1
  4. package/dist/backend/claude-code.js.map +1 -1
  5. package/dist/backend/codex-session-lookup.d.ts +35 -0
  6. package/dist/backend/codex-session-lookup.js +93 -0
  7. package/dist/backend/codex-session-lookup.js.map +1 -1
  8. package/dist/backend/codex.d.ts +27 -0
  9. package/dist/backend/codex.js +116 -20
  10. package/dist/backend/codex.js.map +1 -1
  11. package/dist/backend/grok.d.ts +2 -0
  12. package/dist/backend/grok.js +22 -0
  13. package/dist/backend/grok.js.map +1 -1
  14. package/dist/backend/types.d.ts +2 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/channel/adapters/discord.js +3 -1
  17. package/dist/channel/adapters/discord.js.map +1 -1
  18. package/dist/channel/adapters/telegram.js +3 -1
  19. package/dist/channel/adapters/telegram.js.map +1 -1
  20. package/dist/channel/inbox-temp-name.d.ts +9 -0
  21. package/dist/channel/inbox-temp-name.js +13 -0
  22. package/dist/channel/inbox-temp-name.js.map +1 -0
  23. package/dist/channel/mcp-tools.js +1 -1
  24. package/dist/channel/mcp-tools.js.map +1 -1
  25. package/dist/daemon.d.ts +10 -1
  26. package/dist/daemon.js +196 -26
  27. package/dist/daemon.js.map +1 -1
  28. package/dist/delivery-outbox.test.js +2 -2
  29. package/dist/delivery-outbox.test.js.map +1 -1
  30. package/dist/dialog-screen-text.d.ts +16 -0
  31. package/dist/dialog-screen-text.js +51 -0
  32. package/dist/dialog-screen-text.js.map +1 -0
  33. package/dist/fleet-manager.d.ts +4 -0
  34. package/dist/fleet-manager.js +46 -12
  35. package/dist/fleet-manager.js.map +1 -1
  36. package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
  37. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -2
  38. package/dist/install-stream.d.ts +53 -0
  39. package/dist/install-stream.js +134 -0
  40. package/dist/install-stream.js.map +1 -0
  41. package/dist/instance-lifecycle.d.ts +12 -0
  42. package/dist/instance-lifecycle.js +31 -2
  43. package/dist/instance-lifecycle.js.map +1 -1
  44. package/dist/locale.js +26 -6
  45. package/dist/locale.js.map +1 -1
  46. package/dist/log-service.d.ts +2 -0
  47. package/dist/log-service.js +7 -3
  48. package/dist/log-service.js.map +1 -1
  49. package/dist/login-manager.d.ts +6 -0
  50. package/dist/login-manager.js +10 -1
  51. package/dist/login-manager.js.map +1 -1
  52. package/dist/office-share-page.d.ts +1 -1
  53. package/dist/office-share-page.js +6 -2
  54. package/dist/office-share-page.js.map +1 -1
  55. package/dist/outbound-handlers.js +56 -0
  56. package/dist/outbound-handlers.js.map +1 -1
  57. package/dist/outbound-schemas.d.ts +1 -0
  58. package/dist/outbound-schemas.js +4 -1
  59. package/dist/outbound-schemas.js.map +1 -1
  60. package/dist/selection-card.d.ts +2 -3
  61. package/dist/selection-card.js +99 -26
  62. package/dist/selection-card.js.map +1 -1
  63. package/dist/tmux-manager.d.ts +6 -0
  64. package/dist/tmux-manager.js +17 -0
  65. package/dist/tmux-manager.js.map +1 -1
  66. package/dist/ui/chat-store.js +20 -3
  67. package/dist/ui/fleet-canvas.js +4 -3
  68. package/dist/ui/fleet-office-model.js +55 -22
  69. package/dist/ui/fleet-office-overview.js +6 -38
  70. package/dist/ui/fleet-office-pack.js +4 -3
  71. package/dist/ui/fleet-office-skin.js +114 -21
  72. package/dist/ui/fleet-office-store.js +5 -0
  73. package/dist/ui/fleet-office-zones.js +2 -2
  74. package/dist/ui/fleet-office.js +41 -9
  75. package/dist/ui/panel-log.js +7 -1
  76. package/dist/ui/shared/app-i18n.js +2 -2
  77. package/dist/ui/shared/app-shell.js +4 -2
  78. package/dist/ui/shared/app-stream.js +26 -0
  79. package/dist/ui/shared/app.css +21 -1
  80. package/dist/ui/shared/office/guild.png +0 -0
  81. package/dist/ui/shared/office/school.png +0 -0
  82. package/dist/ui/shared/panel-view.js +185 -10
  83. package/dist/ui/shared/view-strings.js +23 -0
  84. package/dist/unattended-install.d.ts +6 -0
  85. package/dist/unattended-install.js +65 -0
  86. package/dist/unattended-install.js.map +1 -0
  87. package/dist/view-api.d.ts +15 -0
  88. package/dist/view-api.js +94 -33
  89. package/dist/view-api.js.map +1 -1
  90. package/dist/web-api.js +7 -2
  91. package/dist/web-api.js.map +1 -1
  92. package/dist/web-chat-history.d.ts +20 -0
  93. package/dist/web-chat-history.js +21 -0
  94. package/dist/web-chat-history.js.map +1 -1
  95. package/dist/web-guest-access.js +5 -1
  96. package/dist/web-guest-access.js.map +1 -1
  97. package/dist/web-office-pack-tools.d.ts +1 -1
  98. package/dist/web-office-pack-tools.js +1 -1
  99. package/dist/web-office-pack-tools.js.map +1 -1
  100. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { dialogScreenText } from "./dialog-screen-text.js";
1
2
  import { readActiveKiroModel } from "./backend/kiro.js";
2
3
  import { dashboardAccessOf } from "./dashboard-permissions.js";
3
4
  import { sendPackSources } from "./web-office-pack-read.js";
@@ -531,6 +532,9 @@ function countOccurrences(haystack, needle) {
531
532
  }
532
533
  /** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
533
534
  const BOTTOM_READY_POLL_MS = 250;
535
+ /** M-2267: how often a pane left in a tmux mode is looked at again, and how long the delivery Enter waits for it to end. */
536
+ const PANE_MODE_POLL_MS = 500;
537
+ const PANE_MODE_ENTER_WAIT_MS = 10 * 60_000;
534
538
  /** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
535
539
  const DIALOG_PROBE_UNKNOWN_MAX = 40;
536
540
  /** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
@@ -3706,7 +3710,9 @@ export class Daemon extends EventEmitter {
3706
3710
  const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
3707
3711
  if (interactivePrompt) {
3708
3712
  this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
3709
- this.emit("interactive_prompt", { name: this.name, ...interactivePrompt });
3713
+ // M-2269: the prompt as it was on screen, from this capture, for the Log.
3714
+ const screen = dialogScreenText(pane);
3715
+ this.emit("interactive_prompt", { name: this.name, ...interactivePrompt, ...(screen ? { screen } : {}) });
3710
3716
  // A prompt is not an error and must not enter the PTY recovery gate.
3711
3717
  // Continue scanning real errors in this same snapshot.
3712
3718
  }
@@ -3901,7 +3907,7 @@ export class Daemon extends EventEmitter {
3901
3907
  else if (dialog.verifyAfterKeys && !resolved) {
3902
3908
  this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
3903
3909
  }
3904
- this.trackDialogParked(blockingSeen);
3910
+ this.trackDialogParked(blockingSeen, pane);
3905
3911
  return; // Dialog handled (or deliberately deferred): skip error checks this cycle
3906
3912
  }
3907
3913
  // Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
@@ -3911,7 +3917,7 @@ export class Daemon extends EventEmitter {
3911
3917
  this.dialogAnswers = null;
3912
3918
  this.retireParkedNotice("answer");
3913
3919
  }
3914
- this.trackDialogParked(blockingSeen);
3920
+ this.trackDialogParked(blockingSeen, pane);
3915
3921
  this.trackRemoteCard(blockingSeen, pane, captureMono);
3916
3922
  if (blockingSeen)
3917
3923
  return; // held dialog: skip error checks this cycle
@@ -7768,6 +7774,9 @@ export class Daemon extends EventEmitter {
7768
7774
  // The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
7769
7775
  if (written === "handoff-box-unread")
7770
7776
  return "dialog";
7777
+ // M-2267: the pane is in a tmux mode: nothing written — wait for readiness (it holds while the mode lasts), retry.
7778
+ if (written === "pane-in-mode")
7779
+ return "dialog";
7771
7780
  // Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
7772
7781
  return written === false && verdict.fenced ? stale() : written;
7773
7782
  });
@@ -8241,7 +8250,7 @@ export class Daemon extends EventEmitter {
8241
8250
  && this.hasAutoDialogClaim(dialog)
8242
8251
  && dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
8243
8252
  continue;
8244
- this.trackDialogParked(dialog);
8253
+ this.trackDialogParked(dialog, pane);
8245
8254
  this.trackRemoteCard(dialog, pane, probeMono);
8246
8255
  return { state: "dialog", dialog };
8247
8256
  }
@@ -8620,7 +8629,8 @@ export class Daemon extends EventEmitter {
8620
8629
  this.parkedNoticeOut = null;
8621
8630
  this.emit("dialog_unparked", { name: this.name });
8622
8631
  }
8623
- trackDialogParked(dialog) {
8632
+ /** `pane`: the capture the caller already holds — M-2269 keeps the dialog's text from it for the Log (no new capture). */
8633
+ trackDialogParked(dialog, pane = "") {
8624
8634
  if (!dialog) {
8625
8635
  if (this.dialogParkedSince !== 0)
8626
8636
  this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
@@ -8658,7 +8668,9 @@ export class Daemon extends EventEmitter {
8658
8668
  this.dialogParkedReported = true;
8659
8669
  this.parkedNoticeOut = "parked";
8660
8670
  this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
8671
+ const screen = dialogScreenText(pane);
8661
8672
  this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
8673
+ ...(screen ? { screen } : {}),
8662
8674
  ...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
8663
8675
  ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
8664
8676
  ...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
@@ -9018,6 +9030,14 @@ export class Daemon extends EventEmitter {
9018
9030
  : await this.waitForPaneIdleForDelivery(windowId, remaining);
9019
9031
  if (idle !== true)
9020
9032
  return idle;
9033
+ // M-2267: a person left the pane in a tmux mode (copy-mode): its keys would go to the mode. Hold — never take the
9034
+ // pane out of it — and look again; the delivery goes once the mode ends (or this wait's own deadline).
9035
+ if (await this.paneInTmuxMode("readiness")) {
9036
+ if (stop && await stop())
9037
+ return "stopped";
9038
+ await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
9039
+ continue;
9040
+ }
9021
9041
  // The idle wait just resolved, so re-reading the silence gate would only
9022
9042
  // add a stale-cache race; ask the pane the remaining questions directly.
9023
9043
  const transient = await this.probeInputTransient();
@@ -9563,17 +9583,133 @@ export class Daemon extends EventEmitter {
9563
9583
  * precisely so the lock's scope is visible at the call site rather than being
9564
9584
  * an invariant maintained by comments.
9565
9585
  */
9566
- async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
9586
+ async sendDeliveryEnter(phase, stillCurrent, durableAttempt, onInputHeld) {
9567
9587
  if (!(await this.waitForInputTransientToClear(phase)))
9568
9588
  return false;
9569
- return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt);
9589
+ return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld);
9590
+ }
9591
+ /**
9592
+ * M-2267 (hmsg-b8a170c0): is the pane in a tmux mode (a person left it in copy-mode)? Logged once per stretch. Unknown
9593
+ * (unreadable) is not a mode: the delivery's own reads decide then, as before.
9594
+ */
9595
+ paneModeSince = 0;
9596
+ async paneInTmuxMode(phase) {
9597
+ const inMode = await this.tmux?.paneInMode?.();
9598
+ if (inMode !== true) {
9599
+ this.paneModeSince = 0;
9600
+ return false;
9601
+ }
9602
+ if (!this.paneModeSince) {
9603
+ this.paneModeSince = performance.now();
9604
+ this.logger.warn({ phase }, "The pane is in a tmux mode (copy-mode) — delivery waits for it to end; no key is sent to it");
9605
+ }
9606
+ return true;
9607
+ }
9608
+ /** M-2267: wait while the pane is in a tmux mode, until `deadline` (PANE_MODE_ENTER_WAIT_MS) — "clear", "stale" (fenced), "held". */
9609
+ async waitWhilePaneInMode(phase, current, deadline = performance.now() + PANE_MODE_ENTER_WAIT_MS) {
9610
+ for (;;) {
9611
+ const inMode = await this.paneInTmuxMode(phase);
9612
+ if (!current())
9613
+ return "stale";
9614
+ if (!inMode)
9615
+ return "clear";
9616
+ if (performance.now() >= deadline)
9617
+ return "held";
9618
+ await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9619
+ if (!current())
9620
+ return "stale";
9621
+ }
9570
9622
  }
9571
9623
  /** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
9572
- async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt) {
9624
+ async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
9573
9625
  // A recovery Enter may have waited for a transient while cancel or spawn
9574
9626
  // replaced the delivery. Check at the last point before the tmux write.
9575
- if (this.deliveryWritesStopping || (stillCurrent && !stillCurrent()))
9627
+ const tmux = this.tmux;
9628
+ const backend = this.backend;
9629
+ const generation = this.spawnGeneration;
9630
+ 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
9634
+ && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
9635
+ if (!current())
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.
9639
+ const hold = () => {
9640
+ onInputHeld?.();
9641
+ this.logger.warn({ phase }, "Delivery input is not a verified composer; Enter withheld");
9576
9642
  return false;
9643
+ };
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).
9648
+ const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9649
+ for (;;) {
9650
+ const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
9651
+ if (mode === "stale")
9652
+ return false;
9653
+ if (mode === "held")
9654
+ 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
+ if (await this.paneInTmuxMode(phase)) {
9700
+ if (!current())
9701
+ return false;
9702
+ if (performance.now() >= modeDeadline)
9703
+ return hold();
9704
+ await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9705
+ if (!current())
9706
+ return false;
9707
+ continue;
9708
+ }
9709
+ if (!current())
9710
+ return false;
9711
+ break;
9712
+ }
9577
9713
  // W1: persist the "Enter may have happened" boundary before tmux receives
9578
9714
  // the key. A crash after this commit is never auto-replayed without a
9579
9715
  // positive transcript marker, even if the stale pane still shows composer.
@@ -9581,7 +9717,7 @@ export class Daemon extends EventEmitter {
9581
9717
  this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
9582
9718
  return false;
9583
9719
  }
9584
- const sent = await this.tmux.sendSpecialKey("Enter");
9720
+ const sent = await tmux.sendSpecialKey("Enter");
9585
9721
  if (!sent) {
9586
9722
  this.logger.error({
9587
9723
  phase,
@@ -9641,6 +9777,10 @@ export class Daemon extends EventEmitter {
9641
9777
  verdict.fenced = !verdict.durableBeginCommitted;
9642
9778
  return false;
9643
9779
  }
9780
+ // M-2267: the pane went into a tmux mode after the readiness wait. Nothing begun or written: the caller waits for
9781
+ // readiness (which holds while it lasts) and tries again — as for a dialog that appeared before the write.
9782
+ if (!verdict.durableBeginCommitted && await this.paneInTmuxMode("paste"))
9783
+ return "pane-in-mode";
9644
9784
  // A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
9645
9785
  // before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
9646
9786
  // the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
@@ -9737,6 +9877,22 @@ export class Daemon extends EventEmitter {
9737
9877
  // failed/throwing write as uncertain rather than retrying bytes into the
9738
9878
  // same pane; the write-ahead Enter marker later separates safe pre-Enter
9739
9879
  // recovery from a possibly executed command.
9880
+ // M-2267 (#2971 review, Prism P2): EVERY paste attempt — a retry after a failed paste, a first paste whose durable
9881
+ // begin already went — asks too. Before the begin: nothing begun, the caller waits for readiness and tries again.
9882
+ // After it: this attempt waits here, keeping its durable attempt; a mode outlasting the wait fails it unwritten.
9883
+ if (!verdict.durableBeginCommitted) {
9884
+ if (await this.paneInTmuxMode("paste"))
9885
+ return "pane-in-mode";
9886
+ }
9887
+ else {
9888
+ const mode = await this.waitWhilePaneInMode("paste", () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping);
9889
+ if (mode === "stale") {
9890
+ verdict.fenced = false;
9891
+ return false;
9892
+ }
9893
+ if (mode === "held")
9894
+ return this.failDelivery(verdict, status, "paste", "pane-in-tmux-mode");
9895
+ }
9740
9896
  const pasteGeneration = this.spawnGeneration;
9741
9897
  beforePaste?.();
9742
9898
  const pasted = await this.tmux.pasteBuffer(formatted);
@@ -9825,15 +9981,22 @@ export class Daemon extends EventEmitter {
9825
9981
  let enterAt = Date.now();
9826
9982
  const enterMono = performance.now();
9827
9983
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
9828
- if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
9829
- if (steerComposerProof && !steerCurrent()) {
9984
+ let inputHeld = false;
9985
+ if (!(await this.sendDeliveryEnter("initial-submit", steerEnterCurrent, durableAttempt, () => { inputHeld = true; }))) {
9986
+ if (inputHeld) {
9987
+ // The paste may still be in the CLI. Keep the existing uncertain path; no false failure or blind replay.
9988
+ verdict.phase = "submit-enter";
9989
+ verdict.proof = "input-not-verified";
9990
+ return false;
9991
+ }
9992
+ if (!steerCurrent()) {
9830
9993
  verdict.phase = "submit-enter";
9831
9994
  verdict.proof = "fenced-before-enter";
9832
9995
  return false;
9833
9996
  }
9834
9997
  if (steerComposerProof && performance.now() > steerEnterDeadline)
9835
9998
  return this.failDelivery(verdict, status, "submit-enter", "pre-enter-deadline");
9836
- return this.failDelivery(verdict, status, "submit-enter", "tmux-send-keys-failed");
9999
+ return this.failDelivery(verdict, status, "submit-enter", "enter-not-sent");
9837
10000
  }
9838
10001
  // The proof's whole budget, from the Enter: every capture and dialog probe inside it, a late answer dropped.
9839
10002
  const steerProofDeadline = performance.now() + NATIVE_QUEUE_PASTE_VERIFY_MS + POST_ENTER_PROOF_WINDOW_MS;
@@ -9855,7 +10018,7 @@ export class Daemon extends EventEmitter {
9855
10018
  if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
9856
10019
  await new Promise(r => setTimeout(r, 1_000));
9857
10020
  const retryAt = Date.now();
9858
- if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
10021
+ if (await this.sendDeliveryEnter("queue-less-defensive-retry", steerCurrent)) {
9859
10022
  enterAt = retryAt;
9860
10023
  enterRetry = true;
9861
10024
  this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
@@ -9986,7 +10149,7 @@ export class Daemon extends EventEmitter {
9986
10149
  // again. The prompt is back now, so an Enter can land.
9987
10150
  this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
9988
10151
  const strandedAt = Date.now();
9989
- if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
10152
+ if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", steerCurrent))) {
9990
10153
  return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
9991
10154
  }
9992
10155
  // The recovery Enter can be accepted before Codex paints its new
@@ -10039,7 +10202,7 @@ export class Daemon extends EventEmitter {
10039
10202
  }
10040
10203
  await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
10041
10204
  const retryAt = Date.now();
10042
- if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
10205
+ if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", steerCurrent))) {
10043
10206
  return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
10044
10207
  }
10045
10208
  if (windowId && this.controlClient) {
@@ -10091,7 +10254,7 @@ export class Daemon extends EventEmitter {
10091
10254
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
10092
10255
  const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
10093
10256
  const retryAt = Date.now();
10094
- if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
10257
+ if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", steerCurrent))) {
10095
10258
  return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
10096
10259
  }
10097
10260
  submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
@@ -10179,7 +10342,7 @@ export class Daemon extends EventEmitter {
10179
10342
  }
10180
10343
  // No control client to observe output: fall back to the legacy double-Enter.
10181
10344
  await new Promise(r => setTimeout(r, 1000));
10182
- await this.sendDeliveryEnter("unobserved-defensive-retry");
10345
+ await this.sendDeliveryEnter("unobserved-defensive-retry", steerCurrent);
10183
10346
  if (status)
10184
10347
  this.emit("message_confirmed", status); // ✅ (best-effort)
10185
10348
  }
@@ -10647,20 +10810,27 @@ export class Daemon extends EventEmitter {
10647
10810
  * `guard` is for a paste that must not outlive the reason it was made (the continue nudge): `accept` judges the ONE
10648
10811
  * capture taken here — the same picture is the baseline, the expected-screen check and the idle check — and `current`
10649
10812
  * is asked again after every await that precedes a write, so a cancelled paste adds no further paste and no key
10650
- * (the retries included). Without a guard it is exactly the old unconditional path.
10813
+ * (the retries included). Every system paste also keeps the pane owner from before its first await.
10651
10814
  */
10652
10815
  async submitSystemPaste(text, label, guard, admission) {
10653
- const current = guard || admission ? () => { admission?.(); return guard?.current() ?? true; } : undefined;
10654
- if (current && !current())
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())
10655
10825
  return false;
10656
- if (!this.tmux)
10826
+ if (!tmux)
10657
10827
  return false;
10658
10828
  const signature = this.submissionSignature(text);
10659
10829
  let baseline;
10660
10830
  if (guard) {
10661
10831
  let pane;
10662
10832
  try {
10663
- pane = await this.tmux.capturePane();
10833
+ pane = await tmux.capturePane();
10664
10834
  }
10665
10835
  catch {
10666
10836
  return false;
@@ -10675,7 +10845,7 @@ export class Daemon extends EventEmitter {
10675
10845
  if (current && !current())
10676
10846
  return false;
10677
10847
  const pasteGeneration = this.spawnGeneration;
10678
- const pasted = current ? await this.tmux.pasteBuffer(text, { guard: current }) : await this.tmux.pasteBuffer(text);
10848
+ const pasted = current ? await tmux.pasteBuffer(text, { guard: current }) : await tmux.pasteBuffer(text);
10679
10849
  if (!pasted) {
10680
10850
  this.logger.warn({ label }, "System paste failed to reach the pane");
10681
10851
  return false;
@@ -12890,7 +13060,7 @@ export class Daemon extends EventEmitter {
12890
13060
  // ones included: if the auto-dismiss keeps failing here, the human
12891
13061
  // report should count from first sight, not from the runtime scan.
12892
13062
  if (!dialog.fatal && this.deliveryBlockingDialogs().some(d => Daemon.dialogKey(d) === Daemon.dialogKey(dialog))) {
12893
- this.trackDialogParked(dialog);
13063
+ this.trackDialogParked(dialog, pane);
12894
13064
  }
12895
13065
  // A fatal startup screen cannot be dismissed by keypresses (e.g. a
12896
13066
  // corrupt claude.json modal offering only "exit" / "reset config").
@@ -12920,7 +13090,7 @@ export class Daemon extends EventEmitter {
12920
13090
  this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
12921
13091
  this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
12922
13092
  }
12923
- this.trackDialogParked(dialog);
13093
+ this.trackDialogParked(dialog, pane);
12924
13094
  matched = true;
12925
13095
  break;
12926
13096
  }