@hyperdrive.bot/paseo-server 0.3.16 → 0.3.18

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.
@@ -62,6 +62,14 @@ export declare class PtyQuery implements AsyncGenerator<SDKMessage, void> {
62
62
  private resumeDialogAt;
63
63
  /** Set when the CLI rejected the just-submitted command; carries the CLI's own reply. */
64
64
  private rejectedCommandNotice;
65
+ /**
66
+ * Raw capture window for echo verification. The rolling tail deliberately drops lines
67
+ * with fewer than three letters as spinner noise, which also swallows the echo of a
68
+ * short message: "> go" never reaches the tail, so verifying against the tail held a
69
+ * two-letter message forever. While this is non-null, appendTerminalTail mirrors every
70
+ * stripped chunk here unfiltered.
71
+ */
72
+ private echoProbe;
65
73
  private interruptPromptAt;
66
74
  constructor(opts: PtyQueryOptions);
67
75
  private emit;
@@ -86,8 +86,15 @@ const SLASH_COMMAND_ECHO_WAIT_MS = 3000;
86
86
  * which is exactly what makes retyping safe rather than a duplication risk.
87
87
  */
88
88
  const ECHO_VERIFY_WINDOW_MS = 1500;
89
- /** Minimum PTY bytes that count as an echo; a real input-box repaint is far larger. */
90
- const ECHO_MIN_BYTES = 8;
89
+ /**
90
+ * How much of the typed text must be found in the terminal output for it to count as an
91
+ * echo. Byte-counting was not enough: a freshly spawned claude under load keeps painting
92
+ * boot output (splash, MCP connects) after the readiness gate settles, and those bytes
93
+ * passed the old any-bytes check while the input loop was still dead -- three scheduled
94
+ * spawns failed that way in five minutes on a loaded box, 2026-08-03. Boot output never
95
+ * contains the user's message; a real input-box echo always does.
96
+ */
97
+ const ECHO_TEXT_FRAGMENT_CHARS = 24;
91
98
  /** Backoff between retype attempts against a deaf terminal. */
92
99
  const DEAF_RETRY_INITIAL_MS = 5000;
93
100
  const DEAF_RETRY_MAX_MS = 60000;
@@ -238,6 +245,14 @@ export class PtyQuery {
238
245
  this.resumeDialogAt = 0;
239
246
  /** Set when the CLI rejected the just-submitted command; carries the CLI's own reply. */
240
247
  this.rejectedCommandNotice = null;
248
+ /**
249
+ * Raw capture window for echo verification. The rolling tail deliberately drops lines
250
+ * with fewer than three letters as spinner noise, which also swallows the echo of a
251
+ * short message: "> go" never reaches the tail, so verifying against the tail held a
252
+ * two-letter message forever. While this is non-null, appendTerminalTail mirrors every
253
+ * stripped chunk here unfiltered.
254
+ */
255
+ this.echoProbe = null;
241
256
  this.interruptPromptAt = 0;
242
257
  this.transport = opts.transport;
243
258
  this.input = opts.input;
@@ -608,9 +623,22 @@ export class PtyQuery {
608
623
  let attempt = 0;
609
624
  while (!this.done && monotonicNowMs() < deadline) {
610
625
  attempt += 1;
611
- const bytesBefore = this.ptyBytesTotal;
612
- await this.transport.write(text);
613
- const echoed = await pollUntil(() => this.ptyBytesTotal - bytesBefore >= ECHO_MIN_BYTES, ECHO_VERIFY_WINDOW_MS, 100);
626
+ // The needle is whitespace-stripped on both sides: the TUI wraps and re-spaces the
627
+ // echoed text freely (recordings show "Replywithexactly:PROBE_OK"), so only the
628
+ // character sequence survives rendering, never the spacing. Verified against a RAW
629
+ // probe buffer, never the rolling tail: the tail's noise filter drops lines with
630
+ // fewer than three letters, which swallowed the echo of a short message outright.
631
+ const needle = text.replace(/\s+/g, "").slice(0, ECHO_TEXT_FRAGMENT_CHARS);
632
+ this.echoProbe = { buf: "" };
633
+ let echoed = false;
634
+ try {
635
+ await this.transport.write(text);
636
+ const probe = this.echoProbe;
637
+ echoed = await pollUntil(() => (needle ? probe.buf.includes(needle) : probe.buf.length > 0), ECHO_VERIFY_WINDOW_MS, 100);
638
+ }
639
+ finally {
640
+ this.echoProbe = null;
641
+ }
614
642
  if (echoed) {
615
643
  if (attempt > 1) {
616
644
  this.recordLine(`--- terminal came back after ${attempt - 1} deaf attempt(s); prompt delivered ---`);
@@ -856,6 +884,9 @@ export class PtyQuery {
856
884
  const text = stripTerminalControl(chunk);
857
885
  if (!text)
858
886
  return;
887
+ if (this.echoProbe) {
888
+ this.echoProbe.buf = (this.echoProbe.buf + text.replace(/\s+/g, "")).slice(-2000);
889
+ }
859
890
  // Read the busy marker off the raw stripped text, NOT the filtered tail below:
860
891
  // isProgressNoise() drops a line that is only the footer, which is exactly the frame
861
892
  // that proves a turn is running.