@hyperdrive.bot/paseo-server 0.3.37 → 0.3.39

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 (71) hide show
  1. package/dist/server/server/agent/providers/claude/agent.d.ts +10 -0
  2. package/dist/server/server/agent/providers/claude/agent.js +17 -0
  3. package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +2 -0
  4. package/dist/server/server/agent/providers/claude/pty-session-launcher.js +1 -0
  5. package/dist/server/server/agent/providers/claude/transport/pty-query.d.ts +26 -0
  6. package/dist/server/server/agent/providers/claude/transport/pty-query.js +108 -6
  7. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-ca0b24dda014cbbe8a2642bd9af67472.js → desktop-attachment-bridge-a50033674b896f739a450ea6663b581b.js} +5 -5
  8. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a50033674b896f739a450ea6663b581b.js.br +0 -0
  9. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a50033674b896f739a450ea6663b581b.js.gz +0 -0
  10. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a50033674b896f739a450ea6663b581b.js.map.br +0 -0
  11. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a50033674b896f739a450ea6663b581b.js.map.gz +0 -0
  12. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-772a640bc24e7edf384fb56d198ec1b5.js → desktop-attachment-store-fd5764cba5bb72838c62c5a3c701bea1.js} +3 -3
  13. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-fd5764cba5bb72838c62c5a3c701bea1.js.br +0 -0
  14. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-fd5764cba5bb72838c62c5a3c701bea1.js.gz +0 -0
  15. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-fd5764cba5bb72838c62c5a3c701bea1.js.map.br +0 -0
  16. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-fd5764cba5bb72838c62c5a3c701bea1.js.map.gz +0 -0
  17. package/dist/server/web-ui/_expo/static/js/web/{index-57ed87071e3df45480e3c20e6e4c926d.js → index-46b675f5daddb88c514f0eee26695f71.js} +1323 -1322
  18. package/dist/server/web-ui/_expo/static/js/web/index-46b675f5daddb88c514f0eee26695f71.js.br +0 -0
  19. package/dist/server/web-ui/_expo/static/js/web/index-46b675f5daddb88c514f0eee26695f71.js.gz +0 -0
  20. package/dist/server/web-ui/_expo/static/js/web/index-46b675f5daddb88c514f0eee26695f71.js.map.br +0 -0
  21. package/dist/server/web-ui/_expo/static/js/web/{index-57ed87071e3df45480e3c20e6e4c926d.js.map.gz → index-46b675f5daddb88c514f0eee26695f71.js.map.gz} +0 -0
  22. package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-926f0030be75a7025e895c75f4b85a85.js → indexeddb-attachment-store-dd5ff961ec8c17df2652edf5ad3a445d.js} +3 -3
  23. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-dd5ff961ec8c17df2652edf5ad3a445d.js.br +0 -0
  24. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-dd5ff961ec8c17df2652edf5ad3a445d.js.gz +0 -0
  25. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-dd5ff961ec8c17df2652edf5ad3a445d.js.map.br +0 -0
  26. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-dd5ff961ec8c17df2652edf5ad3a445d.js.map.gz +0 -0
  27. package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-2533e8c49bacf836af66efee39221288.js → native-file-attachment-store-3c9685afc8b167936544f93e1997acb6.js} +5 -5
  28. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-3c9685afc8b167936544f93e1997acb6.js.br +0 -0
  29. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-3c9685afc8b167936544f93e1997acb6.js.gz +0 -0
  30. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-3c9685afc8b167936544f93e1997acb6.js.map.br +0 -0
  31. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-3c9685afc8b167936544f93e1997acb6.js.map.gz +0 -0
  32. package/dist/server/web-ui/_expo/static/js/web/{profiler-68be8bb2b97a9fed728062ccc384203e.js → profiler-a9a6c61297c2e9e84bdf23826f88f8fb.js} +11 -11
  33. package/dist/server/web-ui/_expo/static/js/web/profiler-a9a6c61297c2e9e84bdf23826f88f8fb.js.br +0 -0
  34. package/dist/server/web-ui/_expo/static/js/web/profiler-a9a6c61297c2e9e84bdf23826f88f8fb.js.gz +0 -0
  35. package/dist/server/web-ui/_expo/static/js/web/profiler-a9a6c61297c2e9e84bdf23826f88f8fb.js.map.br +0 -0
  36. package/dist/server/web-ui/_expo/static/js/web/profiler-a9a6c61297c2e9e84bdf23826f88f8fb.js.map.gz +0 -0
  37. package/dist/server/web-ui/_expo/static/js/web/{startRecording-5856a1607a3cafa2e7768e6f36918cce.js → startRecording-172521d053daae12167e1ba74a7f1ee4.js} +60 -60
  38. package/dist/server/web-ui/_expo/static/js/web/startRecording-172521d053daae12167e1ba74a7f1ee4.js.br +0 -0
  39. package/dist/server/web-ui/_expo/static/js/web/startRecording-172521d053daae12167e1ba74a7f1ee4.js.gz +0 -0
  40. package/dist/server/web-ui/_expo/static/js/web/startRecording-172521d053daae12167e1ba74a7f1ee4.js.map.br +0 -0
  41. package/dist/server/web-ui/_expo/static/js/web/{startRecording-5856a1607a3cafa2e7768e6f36918cce.js.map.gz → startRecording-172521d053daae12167e1ba74a7f1ee4.js.map.gz} +0 -0
  42. package/dist/server/web-ui/index.html +1 -1
  43. package/dist/server/web-ui/index.html.br +0 -0
  44. package/dist/server/web-ui/index.html.gz +0 -0
  45. package/package.json +6 -6
  46. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-ca0b24dda014cbbe8a2642bd9af67472.js.br +0 -0
  47. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-ca0b24dda014cbbe8a2642bd9af67472.js.gz +0 -0
  48. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-ca0b24dda014cbbe8a2642bd9af67472.js.map.br +0 -0
  49. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-ca0b24dda014cbbe8a2642bd9af67472.js.map.gz +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-772a640bc24e7edf384fb56d198ec1b5.js.br +0 -0
  51. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-772a640bc24e7edf384fb56d198ec1b5.js.gz +0 -0
  52. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-772a640bc24e7edf384fb56d198ec1b5.js.map.br +0 -0
  53. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-772a640bc24e7edf384fb56d198ec1b5.js.map.gz +0 -0
  54. package/dist/server/web-ui/_expo/static/js/web/index-57ed87071e3df45480e3c20e6e4c926d.js.br +0 -0
  55. package/dist/server/web-ui/_expo/static/js/web/index-57ed87071e3df45480e3c20e6e4c926d.js.gz +0 -0
  56. package/dist/server/web-ui/_expo/static/js/web/index-57ed87071e3df45480e3c20e6e4c926d.js.map.br +0 -0
  57. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-926f0030be75a7025e895c75f4b85a85.js.br +0 -0
  58. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-926f0030be75a7025e895c75f4b85a85.js.gz +0 -0
  59. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-926f0030be75a7025e895c75f4b85a85.js.map.br +0 -0
  60. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-926f0030be75a7025e895c75f4b85a85.js.map.gz +0 -0
  61. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-2533e8c49bacf836af66efee39221288.js.br +0 -0
  62. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-2533e8c49bacf836af66efee39221288.js.gz +0 -0
  63. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-2533e8c49bacf836af66efee39221288.js.map.br +0 -0
  64. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-2533e8c49bacf836af66efee39221288.js.map.gz +0 -0
  65. package/dist/server/web-ui/_expo/static/js/web/profiler-68be8bb2b97a9fed728062ccc384203e.js.br +0 -0
  66. package/dist/server/web-ui/_expo/static/js/web/profiler-68be8bb2b97a9fed728062ccc384203e.js.gz +0 -0
  67. package/dist/server/web-ui/_expo/static/js/web/profiler-68be8bb2b97a9fed728062ccc384203e.js.map.br +0 -0
  68. package/dist/server/web-ui/_expo/static/js/web/profiler-68be8bb2b97a9fed728062ccc384203e.js.map.gz +0 -0
  69. package/dist/server/web-ui/_expo/static/js/web/startRecording-5856a1607a3cafa2e7768e6f36918cce.js.br +0 -0
  70. package/dist/server/web-ui/_expo/static/js/web/startRecording-5856a1607a3cafa2e7768e6f36918cce.js.gz +0 -0
  71. package/dist/server/web-ui/_expo/static/js/web/startRecording-5856a1607a3cafa2e7768e6f36918cce.js.map.br +0 -0
@@ -296,6 +296,16 @@ export declare class ClaudeAgentSession implements AgentSession {
296
296
  * Scoped to questions on purpose: a plan dialog's options are the CLI's and version-
297
297
  * dependent, so guessing an index there could approve an implementation nobody chose.
298
298
  */
299
+ /**
300
+ * Surface a delivery-path notice to whoever sent the message.
301
+ *
302
+ * Uses the same synthetic-timeline-message channel executeRewindTurn() already uses, so
303
+ * this needs no protocol change and renders in every client that renders a turn today.
304
+ * The alternative, PtyQuery.emit(), is the SDK message stream, where the only shapes
305
+ * available are `result` messages -- and a result ENDS the turn, which is the opposite
306
+ * of what a "still queued" notice means.
307
+ */
308
+ private emitPtyNotice;
299
309
  private handlePtyDialogChange;
300
310
  /** Map the app's chosen answer back to the dialog's option index and press the keys. */
301
311
  private answerPtyDialogFromPermission;
@@ -2568,6 +2568,7 @@ export class ClaudeAgentSession {
2568
2568
  : undefined,
2569
2569
  logger: this.logger,
2570
2570
  onDialogChange: (dialog) => this.handlePtyDialogChange(dialog),
2571
+ onNotice: (text) => this.emitPtyNotice(text),
2571
2572
  });
2572
2573
  this.activeTransport = ptySession.transport;
2573
2574
  this.query = ptySession.query;
@@ -3660,6 +3661,22 @@ export class ClaudeAgentSession {
3660
3661
  * Scoped to questions on purpose: a plan dialog's options are the CLI's and version-
3661
3662
  * dependent, so guessing an index there could approve an implementation nobody chose.
3662
3663
  */
3664
+ /**
3665
+ * Surface a delivery-path notice to whoever sent the message.
3666
+ *
3667
+ * Uses the same synthetic-timeline-message channel executeRewindTurn() already uses, so
3668
+ * this needs no protocol change and renders in every client that renders a turn today.
3669
+ * The alternative, PtyQuery.emit(), is the SDK message stream, where the only shapes
3670
+ * available are `result` messages -- and a result ENDS the turn, which is the opposite
3671
+ * of what a "still queued" notice means.
3672
+ */
3673
+ emitPtyNotice(text) {
3674
+ this.notifySubscribers({
3675
+ type: "timeline",
3676
+ provider: "claude",
3677
+ item: { type: "assistant_message", text },
3678
+ });
3679
+ }
3663
3680
  handlePtyDialogChange(dialog) {
3664
3681
  if (!dialog) {
3665
3682
  const pendingId = this.ptyDialogRequestId;
@@ -25,6 +25,8 @@ export interface CreatePtySessionOptions {
25
25
  };
26
26
  /** Surfaces TUI question/plan dialogs so the provider can raise a permission request. */
27
27
  onDialogChange?: (dialog: PtyInteractiveDialog | null) => void;
28
+ /** Sender-facing notice from the delivery path (see PtyQueryOptions.onNotice). */
29
+ onNotice?: (text: string) => void;
28
30
  }
29
31
  export interface PtySession {
30
32
  transport: PtyTransport;
@@ -56,6 +56,7 @@ export async function createPtySession(opts) {
56
56
  sessionId: opts.sessionId,
57
57
  logger: opts.logger,
58
58
  ...(opts.onDialogChange ? { onDialogChange: opts.onDialogChange } : {}),
59
+ ...(opts.onNotice ? { onNotice: opts.onNotice } : {}),
59
60
  });
60
61
  opts.logger.debug({ pid: transport.context().pid, sessionId: opts.sessionId, transcriptPath }, "PTY claude session spawned");
61
62
  return { transport, query, transcriptPath, systemPromptFilePath };
@@ -31,6 +31,16 @@ export interface PtyQueryOptions {
31
31
  * the real options instead of an inert tool chip nobody can answer.
32
32
  */
33
33
  onDialogChange?: (dialog: PtyInteractiveDialog | null) => void;
34
+ /**
35
+ * Called with a short, sender-facing sentence when paseo knows something the person who
36
+ * sent the message cannot see. Today that is exactly one thing: the message was accepted
37
+ * but QUEUED behind a running turn, so the spinner they are looking at means "waiting in
38
+ * line", not "working on it".
39
+ *
40
+ * Everything PtyQuery already knew about this went to recordLine(), which appends to
41
+ * ~/.paseo/pty-recordings/<id>.log and reaches nobody.
42
+ */
43
+ onNotice?: (text: string) => void;
34
44
  }
35
45
  export declare class PtyQuery implements AsyncGenerator<SDKMessage, void> {
36
46
  private readonly transport;
@@ -40,6 +50,9 @@ export declare class PtyQuery implements AsyncGenerator<SDKMessage, void> {
40
50
  private readonly logger;
41
51
  private readonly reader;
42
52
  private readonly onDialogChange;
53
+ private readonly onNotice;
54
+ /** One queued notice per message, not one per retry. Reset by deliverPrompt(). */
55
+ private queuedNoticeSent;
43
56
  private readonly outQueue;
44
57
  private readonly outResolvers;
45
58
  private done;
@@ -192,6 +205,19 @@ export declare class PtyQuery implements AsyncGenerator<SDKMessage, void> {
192
205
  * pasted chip is dropped the same way. Best-effort: a transport that cannot take raw
193
206
  * keys just leaves the composer as it was.
194
207
  */
208
+ /**
209
+ * Is the TUI currently showing its "queued messages" footer?
210
+ *
211
+ * This reads a CLI STATUS LINE, not a rendering of our own text. The distinction matters
212
+ * because this file has (rightly) burned four incidents on trusting renderings as proof
213
+ * of DELIVERY -- bytes, then echoed text, then a raw probe, then a paste chip. Delivery
214
+ * is still proved by one thing only, a matching `user` record. The footer answers a
215
+ * different and much cheaper question: is a turn running right now. The CLI is the
216
+ * authority on that, and it prints the answer.
217
+ */
218
+ private queuedFooterVisible;
219
+ /** Tell the sender their message is in line, once per message. */
220
+ private notifyQueued;
195
221
  private clearComposer;
196
222
  /**
197
223
  * Deliver a prompt and prove it, or say it failed. See docs/pty-delivery.md.
@@ -102,6 +102,33 @@ const RECEIPT_EXACT_MATCH_BELOW_CHARS = 12;
102
102
  const RECEIPT_WINDOW_MS = 4000;
103
103
  /** A queued message can only land when the running turn ends, so wait far longer for it. */
104
104
  const QUEUED_RECEIPT_WINDOW_MS = 10 * 60000;
105
+ /**
106
+ * The TUI's own words for "your message is in the queue".
107
+ *
108
+ * A message typed while a turn is running is QUEUED by claude, and a queued message
109
+ * appends NO `user` record until the running turn ends -- so the one delivery oracle
110
+ * (receiptSince) cannot fire, by construction, for minutes at a time. The old test for
111
+ * "is a turn running" was `lastTranscriptAt > attemptStartedAt`, i.e. "did the transcript
112
+ * grow during my 4s window". That is a proxy, and it reads FALSE whenever the running
113
+ * turn is simply quiet for four seconds (thinking, or one slow tool call). When it reads
114
+ * false the loop concludes the terminal is deaf and retypes.
115
+ *
116
+ * Retyping a QUEUED message is not idempotent: the composer is empty (the text already
117
+ * left it), so Ctrl+U clears nothing and the retype either stacks a second queued copy or
118
+ * concatenates onto text the clear failed to kill. Observed live 2026-08-20 on session
119
+ * 0b1ec622, four retries logged at 18:28:13 / 18:29:59 / 18:30:06 / 18:30:14 / 18:30:27,
120
+ * producing these transcript records:
121
+ *
122
+ * mint another xpmint another xp
123
+ * a new one with another cpf: 76797678153, caixaa new one with another cpf: ...
124
+ *
125
+ * The terminal was stating the answer the whole time. Paseo's own turn heartbeat captured
126
+ * it at 18:30:15: `... Press up to edit queued messages - esc to interrupt`. This reads
127
+ * that footer instead of inferring it from file growth.
128
+ */
129
+ const QUEUED_FOOTER_RE = /Press up to edit queued messages/i;
130
+ /** How much of the tail to scan for the footer. It is repainted on every frame. */
131
+ const QUEUED_FOOTER_SCAN_CHARS = 2000;
105
132
  /** Backoff between retype attempts against a deaf terminal. */
106
133
  const DEAF_RETRY_INITIAL_MS = 2000;
107
134
  const DEAF_RETRY_MAX_MS = 60000;
@@ -211,6 +238,8 @@ function monotonicNowMs() {
211
238
  }
212
239
  export class PtyQuery {
213
240
  constructor(opts) {
241
+ /** One queued notice per message, not one per retry. Reset by deliverPrompt(). */
242
+ this.queuedNoticeSent = false;
214
243
  // output queue → async iterator
215
244
  this.outQueue = [];
216
245
  this.outResolvers = [];
@@ -270,6 +299,7 @@ export class PtyQuery {
270
299
  this.logger = opts.logger;
271
300
  this.stallTimeoutMs = opts.stallTimeoutMs ?? TURN_STALL_TIMEOUT_MS;
272
301
  this.onDialogChange = opts.onDialogChange ?? null;
302
+ this.onNotice = opts.onNotice ?? null;
273
303
  this.reader = new TranscriptSdkReader({
274
304
  transcriptPath: opts.transcriptPath,
275
305
  onMessage: (m) => this.onTranscriptMessage(m),
@@ -788,9 +818,60 @@ export class PtyQuery {
788
818
  * pasted chip is dropped the same way. Best-effort: a transport that cannot take raw
789
819
  * keys just leaves the composer as it was.
790
820
  */
821
+ /**
822
+ * Is the TUI currently showing its "queued messages" footer?
823
+ *
824
+ * This reads a CLI STATUS LINE, not a rendering of our own text. The distinction matters
825
+ * because this file has (rightly) burned four incidents on trusting renderings as proof
826
+ * of DELIVERY -- bytes, then echoed text, then a raw probe, then a paste chip. Delivery
827
+ * is still proved by one thing only, a matching `user` record. The footer answers a
828
+ * different and much cheaper question: is a turn running right now. The CLI is the
829
+ * authority on that, and it prints the answer.
830
+ */
831
+ queuedFooterVisible() {
832
+ return QUEUED_FOOTER_RE.test(this.terminalTail.slice(-QUEUED_FOOTER_SCAN_CHARS));
833
+ }
834
+ /** Tell the sender their message is in line, once per message. */
835
+ notifyQueued() {
836
+ if (this.queuedNoticeSent)
837
+ return;
838
+ this.queuedNoticeSent = true;
839
+ try {
840
+ this.onNotice?.("Your message was accepted and is queued behind the turn this session is already " +
841
+ "running. It will start as soon as that turn finishes.");
842
+ }
843
+ catch (err) {
844
+ this.logger.debug({ err }, "PtyQuery: queued notice callback threw");
845
+ }
846
+ }
791
847
  async clearComposer() {
792
848
  try {
793
- await this.transport.writeRaw?.("\u0015");
849
+ // Ctrl+E (end of line), Ctrl+U (kill to start), Ctrl+K (kill to end).
850
+ //
851
+ // Ctrl+U alone was not enough. In a readline-style buffer it kills from the CURSOR
852
+ // to the start of the line: it empties the composer only when the cursor already
853
+ // sits at the end, and it never touches what is after the cursor. Live 2026-08-20 a
854
+ // retype landed ON TOP of text a Ctrl+U was supposed to have killed, which is how
855
+ // session 0b1ec622 got these into its transcript:
856
+ //
857
+ // mint another xpmint another xp
858
+ // a new one with another cpf: 76797678153, caixaa new one with another cpf: ...
859
+ //
860
+ // This sequence is cursor-position independent and idempotent: all three keys are
861
+ // no-ops on an already empty composer, so it cannot damage the common case. Each
862
+ // gets its own beat, because the TUI only processes keys as fast as its input loop
863
+ // drains them.
864
+ //
865
+ // It makes the clear far more likely to succeed. It does NOT prove the composer is
866
+ // empty, and this file deliberately has no way to prove that: every signal for it is
867
+ // a rendering, and renderings are what four earlier incidents taught it not to
868
+ // trust. The real protection against duplication is not retyping at all once the
869
+ // text has demonstrably left the composer -- the queued-footer guard in
870
+ // deliverPrompt(), which removes the case behind every duplicate observed so far.
871
+ for (const key of ["\u0005", "\u0015", "\u000b"]) {
872
+ await this.transport.writeRaw?.(key);
873
+ await delay(60);
874
+ }
794
875
  await delay(120);
795
876
  }
796
877
  catch (err) {
@@ -809,6 +890,7 @@ export class PtyQuery {
809
890
  * running turn is never retyped - that is the one way to send it twice.
810
891
  */
811
892
  async deliverPrompt(text) {
893
+ this.queuedNoticeSent = false;
812
894
  const fingerprint = PtyQuery.fingerprint(text);
813
895
  const watermark = this.transcriptSize();
814
896
  const deadline = monotonicNowMs() + READY_BUSY_MAX_WAIT_MS;
@@ -833,15 +915,35 @@ export class PtyQuery {
833
915
  return "delivered";
834
916
  }
835
917
  // No receipt. Is a turn actually RUNNING (so our text is queued behind it), or was
836
- // the Enter simply swallowed? File growth cannot tell those apart: claude writes
837
- // `attachment` and other bookkeeping records on its own schedule, and treating that
838
- // as "queued" would sit here for ten minutes while the prompt was never submitted
839
- // (the 2026-08-01 bug, in reverse). Real SDK messages arriving IS a running turn.
840
- if (this.lastTranscriptAt > attemptStartedAt) {
918
+ // the Enter simply swallowed? Two independent signals, ORed on purpose:
919
+ //
920
+ // - the TUI's own queued footer, which is the CLI stating the fact outright, and
921
+ // - real SDK messages arriving during this attempt, the original heuristic.
922
+ //
923
+ // File growth alone cannot tell queued from swallowed: claude writes `attachment`
924
+ // and other bookkeeping records on its own schedule, and treating that as "queued"
925
+ // would sit here for ten minutes while the prompt was never submitted (the
926
+ // 2026-08-01 bug, in reverse). But it also misses the common case, a running turn
927
+ // that is quiet for the four-second receipt window, and the cost of missing it is a
928
+ // retype that corrupts the queued prompt. The footer settles that case directly.
929
+ if (this.queuedFooterVisible() || this.lastTranscriptAt > attemptStartedAt) {
841
930
  this.recordLine("--- no receipt yet; queued behind a running turn ---");
931
+ // Say so where the sender can see it. Once per message, not once per poll.
932
+ this.notifyQueued();
842
933
  const late = await pollUntil(() => this.receiptSince(watermark, fingerprint), QUEUED_RECEIPT_WINDOW_MS, 500);
843
934
  if (late)
844
935
  return "delivered";
936
+ // Queued, and still no receipt after the full window. Do NOT fall through to the
937
+ // retype: the text is demonstrably out of the composer, so retyping cannot undo
938
+ // anything and can only stack a duplicate.
939
+ //
940
+ // "unconfirmed", never "undelivered". They are not interchangeable here:
941
+ // `undelivered` makes the caller emit buildSubmitFailedResult(), whose text is
942
+ // "Nothing was sent, so please send it again" -- a lie about a message sitting in
943
+ // claude's queue, and an instruction that produces the exact duplicate this branch
944
+ // exists to prevent. `unconfirmed` means "accepted, start not observed", which is
945
+ // true, and the stall backstop is already the right net for it.
946
+ return "unconfirmed";
845
947
  }
846
948
  this.logger.warn({ sessionId: this.sessionId, attempt, nextRetryMs: backoff }, "PtyQuery: no delivery receipt for the prompt; retrying");
847
949
  this.recordLine(`--- no delivery receipt (attempt ${attempt}); retrying in ${Math.round(backoff / 1000)}s ---`);
@@ -1,5 +1,5 @@
1
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentBridge=function(){const l=(0,s.createDesktopPreviewUrlResolver)({reader:(0,s.createDesktopFileReader)(),objectUrls:(0,s.createBrowserObjectUrlMinter)()});return{copyFile:t.copyDesktopAttachmentFile,writeBase64:t.writeDesktopAttachmentBase64,writeBytes:t.writeDesktopAttachmentBytes,deleteFile:t.deleteDesktopAttachmentFile,garbageCollect:t.garbageCollectDesktopAttachmentFiles,readFileBase64:s.readDesktopFileBase64,resolvePreviewUrl:t=>l.resolve(t),releasePreviewUrl:t=>l.release(t)}};var t=r(d[0]),s=r(d[1])},4820,[4825,4826]);
2
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.writeDesktopAttachmentBase64=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_base64",{attachmentId:n.attachmentId,base64:n.base64,extension:n.extension??null})},e.writeDesktopAttachmentBytes=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_bytes",{attachmentId:n.attachmentId,bytes:n.bytes,extension:n.extension??null})},e.copyDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("copy_attachment_file",{attachmentId:n.attachmentId,sourcePath:n.sourcePath,extension:n.extension??null})},e.deleteDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("delete_attachment_file",{path:n.path})},e.garbageCollectDesktopAttachmentFiles=async function(n){return await(0,t.invokeDesktopCommand)("garbage_collect_attachment_files",{referencedIds:[...n.referencedIds]})};var t=r(d[0])},4825,[3410]);
3
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.readDesktopFileBase64=s,e.createDesktopFileReader=function(){return{readFileBase64:s}},e.createBrowserObjectUrlMinter=function(){return{tryCreate({mimeType:t,base64:n}){if("undefined"==typeof URL||"function"!=typeof URL.createObjectURL)return null;const o=c(n),s=new ArrayBuffer(o.byteLength);new Uint8Array(s).set(o);const u=new Blob([s],{type:t});return URL.createObjectURL(u)},revoke(t){"undefined"!=typeof URL&&"function"==typeof URL.revokeObjectURL&&URL.revokeObjectURL(t)}}},e.createDesktopPreviewUrlResolver=function(t){const n=new Set;return{async resolve(o){const s=await t.reader.readFileBase64(o.storageKey),c=t.objectUrls.tryCreate({mimeType:o.mimeType,base64:s});return null===c?`data:${o.mimeType};base64,${s}`:(n.add(c),c)},async release({url:o}){n.has(o)&&(n.delete(o),t.objectUrls.revoke(o))}}};var t=r(d[0]),n=r(d[1]),o=r(d[2]);async function s(o){return await(0,n.invokeDesktopCommand)("read_file_base64",{path:(0,t.fileUriToPath)(o)})}function c(t){return new Uint8Array(o.Buffer.from(t,"base64"))}},4826,[4497,3410,52]);
4
- //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-bridge-ca0b24dda014cbbe8a2642bd9af67472.js.map
5
- //# debugId=8119469c-3048-4216-abb8-52f1fd628b1f
1
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentBridge=function(){const l=(0,s.createDesktopPreviewUrlResolver)({reader:(0,s.createDesktopFileReader)(),objectUrls:(0,s.createBrowserObjectUrlMinter)()});return{copyFile:t.copyDesktopAttachmentFile,writeBase64:t.writeDesktopAttachmentBase64,writeBytes:t.writeDesktopAttachmentBytes,deleteFile:t.deleteDesktopAttachmentFile,garbageCollect:t.garbageCollectDesktopAttachmentFiles,readFileBase64:s.readDesktopFileBase64,resolvePreviewUrl:t=>l.resolve(t),releasePreviewUrl:t=>l.release(t)}};var t=r(d[0]),s=r(d[1])},4821,[4826,4827]);
2
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.writeDesktopAttachmentBase64=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_base64",{attachmentId:n.attachmentId,base64:n.base64,extension:n.extension??null})},e.writeDesktopAttachmentBytes=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_bytes",{attachmentId:n.attachmentId,bytes:n.bytes,extension:n.extension??null})},e.copyDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("copy_attachment_file",{attachmentId:n.attachmentId,sourcePath:n.sourcePath,extension:n.extension??null})},e.deleteDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("delete_attachment_file",{path:n.path})},e.garbageCollectDesktopAttachmentFiles=async function(n){return await(0,t.invokeDesktopCommand)("garbage_collect_attachment_files",{referencedIds:[...n.referencedIds]})};var t=r(d[0])},4826,[3410]);
3
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.readDesktopFileBase64=s,e.createDesktopFileReader=function(){return{readFileBase64:s}},e.createBrowserObjectUrlMinter=function(){return{tryCreate({mimeType:t,base64:n}){if("undefined"==typeof URL||"function"!=typeof URL.createObjectURL)return null;const o=c(n),s=new ArrayBuffer(o.byteLength);new Uint8Array(s).set(o);const u=new Blob([s],{type:t});return URL.createObjectURL(u)},revoke(t){"undefined"!=typeof URL&&"function"==typeof URL.revokeObjectURL&&URL.revokeObjectURL(t)}}},e.createDesktopPreviewUrlResolver=function(t){const n=new Set;return{async resolve(o){const s=await t.reader.readFileBase64(o.storageKey),c=t.objectUrls.tryCreate({mimeType:o.mimeType,base64:s});return null===c?`data:${o.mimeType};base64,${s}`:(n.add(c),c)},async release({url:o}){n.has(o)&&(n.delete(o),t.objectUrls.revoke(o))}}};var t=r(d[0]),n=r(d[1]),o=r(d[2]);async function s(o){return await(0,n.invokeDesktopCommand)("read_file_base64",{path:(0,t.fileUriToPath)(o)})}function c(t){return new Uint8Array(o.Buffer.from(t,"base64"))}},4827,[4498,3410,52]);
4
+ //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-bridge-a50033674b896f739a450ea6663b581b.js.map
5
+ //# debugId=9e1e80f7-e500-4016-8ec2-26ccbeaae816
@@ -1,3 +1,3 @@
1
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentStore=function(n){return{storageType:"desktop-file",async save(s){const o=s.id??(0,t.generateAttachmentId)(),l=s.fileName??null;if("file_uri"===s.source.kind)return await c(n,{id:o,uri:s.source.uri,mimeType:s.mimeType,fileName:l});if("data_url"===s.source.kind){const c=(0,t.parseDataUrl)(s.source.dataUrl),p=(0,t.normalizeMimeType)(s.mimeType??c.mimeType);return await y(n,{id:o,base64:c.base64,mimeType:p,fileName:l})}if("bytes"===s.source.kind){const c=(0,t.normalizeMimeType)(s.mimeType);return await p(n,{id:o,bytes:s.source.bytes,mimeType:c,fileName:l})}const f=(0,t.normalizeMimeType)(s.mimeType??s.source.blob.type),u=await(0,t.blobToBase64)(s.source.blob);return await y(n,{id:o,base64:u,mimeType:f,fileName:l})},encodeBase64:async({attachment:t})=>(f(t),await n.readFileBase64(t.storageKey)),resolvePreviewUrl:async({attachment:t})=>(f(t),await n.resolvePreviewUrl(t)),async releasePreviewUrl({attachment:t,url:s}){f(t),await n.releasePreviewUrl({url:s})},async delete({attachment:t}){f(t),await n.deleteFile({path:t.storageKey})},async garbageCollect({referencedIds:t}){await n.garbageCollect({referencedIds:Array.from(t)})}}};var t=r(d[0]);const n={"image/png":".png","image/jpeg":".jpg","image/jpg":".jpg","image/gif":".gif","image/webp":".webp","image/avif":".avif","image/heic":".heic","image/heif":".heif","image/tiff":".tiff","image/bmp":".bmp","image/svg+xml":".svg"};function s(t){const n=t.replace(/\\/g,"/").split("/"),s=n[n.length-1];return s&&s.length>0?s:null}function o(s){const o=(0,t.getFileExtensionFromName)(s.fileName);if(o)return o;const l=(0,t.getFileExtensionFromName)(s.sourcePath);return l||(n[s.mimeType]??".img")}function l(t){return{id:t.id,mimeType:t.mimeType,storageType:"desktop-file",storageKey:t.storagePath,fileName:t.fileName??null,byteSize:t.byteSize,createdAt:Date.now()}}async function c(n,c){const y=(0,t.fileUriToPath)(c.uri),p=c.fileName??s(y),f=(0,t.normalizeMimeType)(c.mimeType),u=o({fileName:p,sourcePath:y,mimeType:f}),T=await n.copyFile({attachmentId:c.id,sourcePath:y,extension:u});return l({id:c.id,mimeType:f,storagePath:T.path,byteSize:T.byteSize,fileName:p})}async function y(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBase64({attachmentId:n.id,base64:n.base64,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}async function p(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBytes({attachmentId:n.id,bytes:n.bytes,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}function f(t){if("desktop-file"!==t.storageType)throw new Error(`Unsupported desktop attachment storage type '${t.storageType}'.`)}},4819,[4497]);
2
- //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-store-772a640bc24e7edf384fb56d198ec1b5.js.map
3
- //# debugId=d0a1beae-1978-4a26-87e2-b76f2c8e5638
1
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentStore=function(n){return{storageType:"desktop-file",async save(s){const o=s.id??(0,t.generateAttachmentId)(),l=s.fileName??null;if("file_uri"===s.source.kind)return await c(n,{id:o,uri:s.source.uri,mimeType:s.mimeType,fileName:l});if("data_url"===s.source.kind){const c=(0,t.parseDataUrl)(s.source.dataUrl),p=(0,t.normalizeMimeType)(s.mimeType??c.mimeType);return await y(n,{id:o,base64:c.base64,mimeType:p,fileName:l})}if("bytes"===s.source.kind){const c=(0,t.normalizeMimeType)(s.mimeType);return await p(n,{id:o,bytes:s.source.bytes,mimeType:c,fileName:l})}const f=(0,t.normalizeMimeType)(s.mimeType??s.source.blob.type),u=await(0,t.blobToBase64)(s.source.blob);return await y(n,{id:o,base64:u,mimeType:f,fileName:l})},encodeBase64:async({attachment:t})=>(f(t),await n.readFileBase64(t.storageKey)),resolvePreviewUrl:async({attachment:t})=>(f(t),await n.resolvePreviewUrl(t)),async releasePreviewUrl({attachment:t,url:s}){f(t),await n.releasePreviewUrl({url:s})},async delete({attachment:t}){f(t),await n.deleteFile({path:t.storageKey})},async garbageCollect({referencedIds:t}){await n.garbageCollect({referencedIds:Array.from(t)})}}};var t=r(d[0]);const n={"image/png":".png","image/jpeg":".jpg","image/jpg":".jpg","image/gif":".gif","image/webp":".webp","image/avif":".avif","image/heic":".heic","image/heif":".heif","image/tiff":".tiff","image/bmp":".bmp","image/svg+xml":".svg"};function s(t){const n=t.replace(/\\/g,"/").split("/"),s=n[n.length-1];return s&&s.length>0?s:null}function o(s){const o=(0,t.getFileExtensionFromName)(s.fileName);if(o)return o;const l=(0,t.getFileExtensionFromName)(s.sourcePath);return l||(n[s.mimeType]??".img")}function l(t){return{id:t.id,mimeType:t.mimeType,storageType:"desktop-file",storageKey:t.storagePath,fileName:t.fileName??null,byteSize:t.byteSize,createdAt:Date.now()}}async function c(n,c){const y=(0,t.fileUriToPath)(c.uri),p=c.fileName??s(y),f=(0,t.normalizeMimeType)(c.mimeType),u=o({fileName:p,sourcePath:y,mimeType:f}),T=await n.copyFile({attachmentId:c.id,sourcePath:y,extension:u});return l({id:c.id,mimeType:f,storagePath:T.path,byteSize:T.byteSize,fileName:p})}async function y(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBase64({attachmentId:n.id,base64:n.base64,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}async function p(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBytes({attachmentId:n.id,bytes:n.bytes,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}function f(t){if("desktop-file"!==t.storageType)throw new Error(`Unsupported desktop attachment storage type '${t.storageType}'.`)}},4820,[4498]);
2
+ //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-store-fd5764cba5bb72838c62c5a3c701bea1.js.map
3
+ //# debugId=0161bfe7-deeb-46d5-9490-d37d91b621c4