crewly 1.20.200 → 1.20.201

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 (114) hide show
  1. package/config/constants.ts +2 -0
  2. package/config/skills/agent/screenshot-compare/execute.sh +8 -2
  3. package/config/skills/agent/screenshot-compare/execute.test.sh +77 -0
  4. package/config/skills/agent/transcribe-audio/SKILL.md +1 -1
  5. package/config/skills/agent/transcribe-audio/execute.sh +13 -4
  6. package/config/skills/agent/transcribe-audio/execute.test.sh +39 -0
  7. package/config/skills/agent/transcribe-audio/instructions.md +1 -1
  8. package/config/skills/registry.json +3 -3
  9. package/dist/backend/backend/src/constants.d.ts +28 -4
  10. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  11. package/dist/backend/backend/src/constants.js +27 -4
  12. package/dist/backend/backend/src/constants.js.map +1 -1
  13. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts +32 -10
  14. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +47 -16
  16. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.d.ts.map +1 -1
  18. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js +20 -7
  19. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js.map +1 -1
  20. package/dist/backend/backend/src/controllers/session/session.controller.d.ts +3 -1
  21. package/dist/backend/backend/src/controllers/session/session.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/session/session.controller.js +7 -2
  23. package/dist/backend/backend/src/controllers/session/session.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/session/session.routes.d.ts.map +1 -1
  25. package/dist/backend/backend/src/controllers/session/session.routes.js +15 -4
  26. package/dist/backend/backend/src/controllers/session/session.routes.js.map +1 -1
  27. package/dist/backend/backend/src/controllers/settings/settings.controller.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/settings/settings.controller.js +91 -16
  29. package/dist/backend/backend/src/controllers/settings/settings.controller.js.map +1 -1
  30. package/dist/backend/backend/src/index.d.ts.map +1 -1
  31. package/dist/backend/backend/src/index.js +4 -0
  32. package/dist/backend/backend/src/index.js.map +1 -1
  33. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +52 -0
  34. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  35. package/dist/backend/backend/src/services/agent/agent-registration.service.js +125 -11
  36. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  37. package/dist/backend/backend/src/services/agent/antigravity-runtime.service.d.ts.map +1 -1
  38. package/dist/backend/backend/src/services/agent/antigravity-runtime.service.js +3 -2
  39. package/dist/backend/backend/src/services/agent/antigravity-runtime.service.js.map +1 -1
  40. package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.d.ts +17 -0
  41. package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.js +23 -0
  43. package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts +14 -1
  45. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -1
  46. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js +23 -1
  47. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -1
  48. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -0
  50. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  51. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +10 -0
  52. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/session/pty/pty-session.js +22 -0
  54. package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
  55. package/dist/backend/backend/src/services/session/session-command-helper.d.ts +109 -15
  56. package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/session/session-command-helper.js +244 -41
  58. package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
  59. package/dist/backend/backend/src/services/session/tui-input-guard.d.ts +57 -0
  60. package/dist/backend/backend/src/services/session/tui-input-guard.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/session/tui-input-guard.js +104 -7
  62. package/dist/backend/backend/src/services/session/tui-input-guard.js.map +1 -1
  63. package/dist/backend/backend/src/types/settings.types.d.ts +24 -0
  64. package/dist/backend/backend/src/types/settings.types.d.ts.map +1 -1
  65. package/dist/backend/backend/src/types/settings.types.js +65 -3
  66. package/dist/backend/backend/src/types/settings.types.js.map +1 -1
  67. package/dist/backend/build-info.json +2 -2
  68. package/dist/backend/config/constants.d.ts +2 -0
  69. package/dist/backend/config/constants.d.ts.map +1 -1
  70. package/dist/backend/config/constants.js +2 -0
  71. package/dist/backend/config/constants.js.map +1 -1
  72. package/dist/cli/backend/src/constants.d.ts +28 -4
  73. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  74. package/dist/cli/backend/src/constants.js +27 -4
  75. package/dist/cli/backend/src/constants.js.map +1 -1
  76. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +52 -0
  77. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  78. package/dist/cli/backend/src/services/agent/agent-registration.service.js +125 -11
  79. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  80. package/dist/cli/backend/src/services/agent/antigravity-runtime.service.d.ts.map +1 -1
  81. package/dist/cli/backend/src/services/agent/antigravity-runtime.service.js +3 -2
  82. package/dist/cli/backend/src/services/agent/antigravity-runtime.service.js.map +1 -1
  83. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts +17 -0
  84. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts.map +1 -1
  85. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js +23 -0
  86. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js.map +1 -1
  87. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts +14 -1
  88. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -1
  89. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js +23 -1
  90. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -1
  91. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  92. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +1 -0
  93. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  94. package/dist/cli/backend/src/services/session/pty/pty-session.d.ts +10 -0
  95. package/dist/cli/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  96. package/dist/cli/backend/src/services/session/pty/pty-session.js +22 -0
  97. package/dist/cli/backend/src/services/session/pty/pty-session.js.map +1 -1
  98. package/dist/cli/backend/src/services/session/session-command-helper.d.ts +109 -15
  99. package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  100. package/dist/cli/backend/src/services/session/session-command-helper.js +244 -41
  101. package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -1
  102. package/dist/cli/backend/src/services/session/tui-input-guard.d.ts +57 -0
  103. package/dist/cli/backend/src/services/session/tui-input-guard.d.ts.map +1 -1
  104. package/dist/cli/backend/src/services/session/tui-input-guard.js +104 -7
  105. package/dist/cli/backend/src/services/session/tui-input-guard.js.map +1 -1
  106. package/dist/cli/backend/src/types/settings.types.d.ts +24 -0
  107. package/dist/cli/backend/src/types/settings.types.d.ts.map +1 -1
  108. package/dist/cli/backend/src/types/settings.types.js +65 -3
  109. package/dist/cli/backend/src/types/settings.types.js.map +1 -1
  110. package/dist/cli/config/constants.d.ts +2 -0
  111. package/dist/cli/config/constants.d.ts.map +1 -1
  112. package/dist/cli/config/constants.js +2 -0
  113. package/dist/cli/config/constants.js.map +1 -1
  114. package/package.json +1 -1
@@ -31,16 +31,36 @@ export declare class SessionCommandHelper {
31
31
  private logger;
32
32
  private backend;
33
33
  /**
34
- * Per session: the collapsed paste marker ("[Pasted text #1 +29 lines]",
35
- * "[Pasted Content 1234 chars]") the runtime showed right after the
36
- * harness pasted `message`. Proof that a marker still in the box later is
37
- * ours, so a lost Enter can be recovered. Shared across helper instances.
38
- */
39
- private static readonly ownPasteMarkers;
40
- /** Clock for the marker record (tests). */
34
+ * Per session: the harness's own latest paste into a box it had proven
35
+ * empty. While the box keeps showing it (the collapsed marker "[Pasted
36
+ * text #4 +5 lines]" / "[Pasted Content 1234 chars]", or the text itself)
37
+ * it is ours — whatever message is sent next — so a lost or never-pressed
38
+ * Enter can be recovered. Written by every paste path; shared across
39
+ * helper instances.
40
+ *
41
+ * - `pending`: pasted, not yet seen in the box (a busy Claude Code renders
42
+ * a paste seconds late). The first readable box that is not empty must
43
+ * show the paste's shape, or the record ends; it also ends unseen after
44
+ * OWN_PASTE_PENDING_MAX_MS.
45
+ * - `seen`: the box showed `shown`. Every readable box since must show
46
+ * exactly that; the first that does not ends the record. Unreadable
47
+ * screens (a dialog) do not count as seen: unseen for
48
+ * OWN_PASTE_UNSEEN_MAX_MS ends it too. While busy the watcher re-reads
49
+ * the box, so a long turn keeps it alive.
50
+ *
51
+ * Also ended by our one Enter on it, and by session/runtime (re)start.
52
+ */
53
+ private static readonly ownPastes;
54
+ /** Sessions a sendMessage is typing into right now (the watcher keeps off) */
55
+ private static readonly inFlight;
56
+ /** The watcher re-reading boxes that hold a paste of ours */
57
+ private static watchTimer;
58
+ /** Clock for the paste record (tests). */
41
59
  static now: () => number;
60
+ /** How long sendMessage waits for a late paste to render (tests shorten it). */
61
+ static pasteRenderMaxWaitMs: number;
42
62
  /**
43
- * Forget the recorded paste marker of a session: its runtime is being
63
+ * Forget the recorded paste of a session: its runtime is being
44
64
  * (re)started, a shell line is typed, or the session is created/killed —
45
65
  * Claude Code's paste counter restarts at #1, and Codex markers carry only
46
66
  * a character count, so an old record could match the owner's paste.
@@ -49,13 +69,58 @@ export declare class SessionCommandHelper {
49
69
  */
50
70
  static forgetOwnPaste(sessionName: string): void;
51
71
  /**
52
- * The recorded marker, while it may still be ours: at most
53
- * OWN_MARKER_TTL_MS after our paste.
72
+ * Whether a paste of ours is on record for a session (tests, diagnostics).
73
+ *
74
+ * @param sessionName - The session
75
+ * @returns True while the record stands
76
+ */
77
+ static hasOwnPaste(sessionName: string): boolean;
78
+ /**
79
+ * Record our paste of `message` into a box proven empty.
80
+ *
81
+ * @param sessionName - The session
82
+ * @param message - What was pasted
83
+ */
84
+ private recordOwnPaste;
85
+ /** Whether recording a paste starts the timed watcher (tests drive it by hand). */
86
+ static autoWatch: boolean;
87
+ /** Start the watcher (no-op when running). */
88
+ private static startWatching;
89
+ /** Stop the watcher once no paste is on record. */
90
+ private static stopWatchingIfIdle;
91
+ /**
92
+ * One watcher pass: re-read every box holding a paste of ours (keeping
93
+ * the "seen continuously" record alive through a long turn, ending it the
94
+ * moment the box shows anything else), and once the agent is idle with
95
+ * our paste still sitting there, press Enter on it once.
96
+ *
97
+ * @returns Resolves when every session was looked at
98
+ */
99
+ static watchOwnPastes(): Promise<void>;
100
+ /** Pause between screen samples when checking that a busy screen repaints (tests shorten it). */
101
+ static repaintSampleMs: number;
102
+ /**
103
+ * Whether the agent in a session is mid-turn. All three must hold:
104
+ * - a turn signal where the runtime paints it (see readTurnSignals): the
105
+ * busy bar in the footer below the box, or the spinner line directly
106
+ * above it — never text in the transcript;
107
+ * - the screen is repainting: it changes within a few short samples (a
108
+ * live spinner animates its glyph and counter several times a second);
109
+ * - the PTY produced output within BUSY_FROZEN_MS: a screen that stopped
110
+ * repainting is frozen, not busy, and a held message must go out.
54
111
  *
55
112
  * @param sessionName - The session
56
- * @returns The marker, or undefined
113
+ * @returns True when busy
57
114
  */
58
- private static ownMarkerFor;
115
+ isAgentBusy(sessionName: string): Promise<boolean>;
116
+ /**
117
+ * Whether the agent in a session is resting: not busy (see
118
+ * {@link isAgentBusy}) and no meaningful output for a few seconds.
119
+ *
120
+ * @param sessionName - The session
121
+ * @returns True when idle
122
+ */
123
+ isAgentIdle(sessionName: string): Promise<boolean>;
59
124
  constructor(backend: ISessionBackend);
60
125
  /**
61
126
  * Get a session by name, throwing an error if it doesn't exist.
@@ -90,6 +155,11 @@ export declare class SessionCommandHelper {
90
155
  * submitted and nothing is cleared: text we cannot prove is ours is
91
156
  * never wiped.
92
157
  *
158
+ * Every paste is recorded (see `ownPastes`): if its Enter is lost, or it
159
+ * renders only after we stopped looking (a busy Claude Code), it stays
160
+ * ours while the box keeps showing it, and is submitted once the agent
161
+ * is idle — by the next delivery or by the watcher.
162
+ *
93
163
  * A refusal throws `TuiInputGuardError`; agent delivery keeps the message
94
164
  * queued and retries (AgentRegistrationService). Shell command lines
95
165
  * typed before a runtime starts use {@link sendShellLine} instead.
@@ -99,9 +169,14 @@ export declare class SessionCommandHelper {
99
169
  * @throws Error if session does not exist
100
170
  * @throws TuiInputGuardError when the box is unreadable or not ours
101
171
  */
102
- sendMessage(sessionName: string, message: string, options?: {
103
- recordPasteMarker?: boolean;
104
- }): Promise<void>;
172
+ sendMessage(sessionName: string, message: string): Promise<void>;
173
+ /**
174
+ * The body of {@link sendMessage}, run while the watcher keeps off.
175
+ *
176
+ * @param sessionName - The session to send to
177
+ * @param message - The message to send
178
+ */
179
+ private sendMessageGuarded;
105
180
  /**
106
181
  * After a delivery: if the box still holds the collapsed marker of our
107
182
  * own paste, the Enter was lost — press Enter once and look again. Runs
@@ -143,6 +218,25 @@ export declare class SessionCommandHelper {
143
218
  * @returns The reading
144
219
  */
145
220
  readInputBox(sessionName: string, message: string, stage?: TuiInputStage): TuiInputReading;
221
+ /**
222
+ * What the box showed for our recorded paste, while the record stands.
223
+ * Ends the record when it has gone unseen too long.
224
+ *
225
+ * @param sessionName - The session
226
+ * @returns The text the box showed, or undefined (none seen / no record)
227
+ */
228
+ private ownPasteShown;
229
+ /**
230
+ * Keep the paste record in step with a reading: the record stands only
231
+ * while every readable box shows our paste. A pending paste (not seen
232
+ * yet) is adopted when the box first shows its shape; an empty box keeps
233
+ * it pending; anything else ends it.
234
+ *
235
+ * @param sessionName - The session
236
+ * @param reading - The classification just made
237
+ * @returns The reading, marked `ownPasteMarker` when the box shows our paste
238
+ */
239
+ private applyOwnPaste;
146
240
  /**
147
241
  * Empty the input box — only call it for text proven to be the harness's
148
242
  * own. Sends Ctrl+U then Backspace (clear the line, join the empty line to
@@ -1 +1 @@
1
- {"version":3,"file":"session-command-helper.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/session/session-command-helper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-command-helper.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/session/session-command-helper.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,7 @@ import { delay } from '../../utils/async.utils.js';
21
21
  import { assertNotSecretEnvKey } from '../../utils/secret-env.js';
22
22
  import { quietShellLine } from '../../utils/shell-history.js';
23
23
  import { PtyActivityTrackerService } from '../agent/pty-activity-tracker.service.js';
24
- import { classifyTuiInput, isPasteMarker, TuiInputGuardError } from './tui-input-guard.js';
24
+ import { classifyTuiInput, pasteShowsAs, screenShowsTurnInProgress, TuiInputGuardError } from './tui-input-guard.js';
25
25
  import { noteHarnessWrite } from '../trace/turn-origin.js';
26
26
  /**
27
27
  * Key code mappings for special keys
@@ -69,16 +69,36 @@ export class SessionCommandHelper {
69
69
  logger;
70
70
  backend;
71
71
  /**
72
- * Per session: the collapsed paste marker ("[Pasted text #1 +29 lines]",
73
- * "[Pasted Content 1234 chars]") the runtime showed right after the
74
- * harness pasted `message`. Proof that a marker still in the box later is
75
- * ours, so a lost Enter can be recovered. Shared across helper instances.
76
- */
77
- static ownPasteMarkers = new Map();
78
- /** Clock for the marker record (tests). */
72
+ * Per session: the harness's own latest paste into a box it had proven
73
+ * empty. While the box keeps showing it (the collapsed marker "[Pasted
74
+ * text #4 +5 lines]" / "[Pasted Content 1234 chars]", or the text itself)
75
+ * it is ours — whatever message is sent next — so a lost or never-pressed
76
+ * Enter can be recovered. Written by every paste path; shared across
77
+ * helper instances.
78
+ *
79
+ * - `pending`: pasted, not yet seen in the box (a busy Claude Code renders
80
+ * a paste seconds late). The first readable box that is not empty must
81
+ * show the paste's shape, or the record ends; it also ends unseen after
82
+ * OWN_PASTE_PENDING_MAX_MS.
83
+ * - `seen`: the box showed `shown`. Every readable box since must show
84
+ * exactly that; the first that does not ends the record. Unreadable
85
+ * screens (a dialog) do not count as seen: unseen for
86
+ * OWN_PASTE_UNSEEN_MAX_MS ends it too. While busy the watcher re-reads
87
+ * the box, so a long turn keeps it alive.
88
+ *
89
+ * Also ended by our one Enter on it, and by session/runtime (re)start.
90
+ */
91
+ static ownPastes = new Map();
92
+ /** Sessions a sendMessage is typing into right now (the watcher keeps off) */
93
+ static inFlight = new Set();
94
+ /** The watcher re-reading boxes that hold a paste of ours */
95
+ static watchTimer = null;
96
+ /** Clock for the paste record (tests). */
79
97
  static now = Date.now;
98
+ /** How long sendMessage waits for a late paste to render (tests shorten it). */
99
+ static pasteRenderMaxWaitMs = TUI_INPUT_GUARD.PASTE_RENDER_MAX_WAIT_MS;
80
100
  /**
81
- * Forget the recorded paste marker of a session: its runtime is being
101
+ * Forget the recorded paste of a session: its runtime is being
82
102
  * (re)started, a shell line is typed, or the session is created/killed —
83
103
  * Claude Code's paste counter restarts at #1, and Codex markers carry only
84
104
  * a character count, so an old record could match the owner's paste.
@@ -86,24 +106,129 @@ export class SessionCommandHelper {
86
106
  * @param sessionName - The session
87
107
  */
88
108
  static forgetOwnPaste(sessionName) {
89
- SessionCommandHelper.ownPasteMarkers.delete(sessionName);
109
+ SessionCommandHelper.ownPastes.delete(sessionName);
110
+ SessionCommandHelper.stopWatchingIfIdle();
90
111
  }
91
112
  /**
92
- * The recorded marker, while it may still be ours: at most
93
- * OWN_MARKER_TTL_MS after our paste.
113
+ * Whether a paste of ours is on record for a session (tests, diagnostics).
94
114
  *
95
115
  * @param sessionName - The session
96
- * @returns The marker, or undefined
116
+ * @returns True while the record stands
97
117
  */
98
- static ownMarkerFor(sessionName) {
99
- const rec = SessionCommandHelper.ownPasteMarkers.get(sessionName);
100
- if (!rec)
101
- return undefined;
102
- if (SessionCommandHelper.now() - rec.at > TUI_INPUT_GUARD.OWN_MARKER_TTL_MS) {
103
- SessionCommandHelper.ownPasteMarkers.delete(sessionName);
104
- return undefined;
118
+ static hasOwnPaste(sessionName) {
119
+ return SessionCommandHelper.ownPastes.has(sessionName);
120
+ }
121
+ /**
122
+ * Record our paste of `message` into a box proven empty.
123
+ *
124
+ * @param sessionName - The session
125
+ * @param message - What was pasted
126
+ */
127
+ recordOwnPaste(sessionName, message) {
128
+ SessionCommandHelper.ownPastes.set(sessionName, { message, pastedAt: SessionCommandHelper.now(), helper: this });
129
+ SessionCommandHelper.startWatching();
130
+ }
131
+ /** Whether recording a paste starts the timed watcher (tests drive it by hand). */
132
+ static autoWatch = true;
133
+ /** Start the watcher (no-op when running). */
134
+ static startWatching() {
135
+ if (SessionCommandHelper.watchTimer || !SessionCommandHelper.autoWatch)
136
+ return;
137
+ SessionCommandHelper.watchTimer = setInterval(() => {
138
+ void SessionCommandHelper.watchOwnPastes();
139
+ }, TUI_INPUT_GUARD.OWN_PASTE_WATCH_MS);
140
+ SessionCommandHelper.watchTimer.unref?.();
141
+ }
142
+ /** Stop the watcher once no paste is on record. */
143
+ static stopWatchingIfIdle() {
144
+ if (SessionCommandHelper.ownPastes.size > 0 || !SessionCommandHelper.watchTimer)
145
+ return;
146
+ clearInterval(SessionCommandHelper.watchTimer);
147
+ SessionCommandHelper.watchTimer = null;
148
+ }
149
+ /**
150
+ * One watcher pass: re-read every box holding a paste of ours (keeping
151
+ * the "seen continuously" record alive through a long turn, ending it the
152
+ * moment the box shows anything else), and once the agent is idle with
153
+ * our paste still sitting there, press Enter on it once.
154
+ *
155
+ * @returns Resolves when every session was looked at
156
+ */
157
+ static async watchOwnPastes() {
158
+ for (const [sessionName, rec] of [...SessionCommandHelper.ownPastes]) {
159
+ if (SessionCommandHelper.inFlight.has(sessionName))
160
+ continue;
161
+ try {
162
+ if (!rec.helper.backend.sessionExists(sessionName)) {
163
+ SessionCommandHelper.ownPastes.delete(sessionName);
164
+ continue;
165
+ }
166
+ const reading = rec.helper.readInputBox(sessionName, '', 'recovery');
167
+ if (reading.ownPasteMarker && await rec.helper.isAgentIdle(sessionName)) {
168
+ const outcome = await rec.helper.ensureOwnPasteSubmitted(sessionName);
169
+ rec.helper.logger.warn('Our earlier paste was still in the input box of an idle agent — pressed Enter on it once', { sessionName, outcome });
170
+ }
171
+ }
172
+ catch {
173
+ // A session that vanished mid-pass: the next pass drops it.
174
+ }
105
175
  }
106
- return rec.marker;
176
+ SessionCommandHelper.stopWatchingIfIdle();
177
+ }
178
+ /** Pause between screen samples when checking that a busy screen repaints (tests shorten it). */
179
+ static repaintSampleMs = TUI_INPUT_GUARD.BUSY_REPAINT_SAMPLE_MS;
180
+ /**
181
+ * Whether the agent in a session is mid-turn. All three must hold:
182
+ * - a turn signal where the runtime paints it (see readTurnSignals): the
183
+ * busy bar in the footer below the box, or the spinner line directly
184
+ * above it — never text in the transcript;
185
+ * - the screen is repainting: it changes within a few short samples (a
186
+ * live spinner animates its glyph and counter several times a second);
187
+ * - the PTY produced output within BUSY_FROZEN_MS: a screen that stopped
188
+ * repainting is frozen, not busy, and a held message must go out.
189
+ *
190
+ * @param sessionName - The session
191
+ * @returns True when busy
192
+ */
193
+ async isAgentBusy(sessionName) {
194
+ const read = () => {
195
+ try {
196
+ return this.backend.captureOutput(sessionName, TUI_INPUT_GUARD.BUSY_BAR_TAIL_LINES) ?? '';
197
+ }
198
+ catch {
199
+ return '';
200
+ }
201
+ };
202
+ const first = read();
203
+ if (!screenShowsTurnInProgress(first))
204
+ return false;
205
+ const tracker = PtyActivityTrackerService.getInstance();
206
+ const quietMs = typeof tracker.getRawOutputIdleMs === 'function' ? tracker.getRawOutputIdleMs(sessionName) : null;
207
+ if (quietMs !== null && quietMs >= TUI_INPUT_GUARD.BUSY_FROZEN_MS) {
208
+ this.logger.warn('Screen shows a turn but the PTY has been silent — treating the agent as idle', { sessionName, quietMs });
209
+ return false;
210
+ }
211
+ for (let i = 0; i < TUI_INPUT_GUARD.BUSY_REPAINT_SAMPLES; i++) {
212
+ await delay(SessionCommandHelper.repaintSampleMs);
213
+ const next = read();
214
+ if (!screenShowsTurnInProgress(next))
215
+ return false;
216
+ if (next !== first)
217
+ return true;
218
+ }
219
+ return false;
220
+ }
221
+ /**
222
+ * Whether the agent in a session is resting: not busy (see
223
+ * {@link isAgentBusy}) and no meaningful output for a few seconds.
224
+ *
225
+ * @param sessionName - The session
226
+ * @returns True when idle
227
+ */
228
+ async isAgentIdle(sessionName) {
229
+ if (await this.isAgentBusy(sessionName))
230
+ return false;
231
+ return PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName) >= SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
107
232
  }
108
233
  constructor(backend) {
109
234
  this.logger = LoggerService.getInstance().createComponentLogger('SessionCommandHelper');
@@ -152,6 +277,11 @@ export class SessionCommandHelper {
152
277
  * submitted and nothing is cleared: text we cannot prove is ours is
153
278
  * never wiped.
154
279
  *
280
+ * Every paste is recorded (see `ownPastes`): if its Enter is lost, or it
281
+ * renders only after we stopped looking (a busy Claude Code), it stays
282
+ * ours while the box keeps showing it, and is submitted once the agent
283
+ * is idle — by the next delivery or by the watcher.
284
+ *
155
285
  * A refusal throws `TuiInputGuardError`; agent delivery keeps the message
156
286
  * queued and retries (AgentRegistrationService). Shell command lines
157
287
  * typed before a runtime starts use {@link sendShellLine} instead.
@@ -161,7 +291,22 @@ export class SessionCommandHelper {
161
291
  * @throws Error if session does not exist
162
292
  * @throws TuiInputGuardError when the box is unreadable or not ours
163
293
  */
164
- async sendMessage(sessionName, message, options = {}) {
294
+ async sendMessage(sessionName, message) {
295
+ SessionCommandHelper.inFlight.add(sessionName);
296
+ try {
297
+ await this.sendMessageGuarded(sessionName, message);
298
+ }
299
+ finally {
300
+ SessionCommandHelper.inFlight.delete(sessionName);
301
+ }
302
+ }
303
+ /**
304
+ * The body of {@link sendMessage}, run while the watcher keeps off.
305
+ *
306
+ * @param sessionName - The session to send to
307
+ * @param message - The message to send
308
+ */
309
+ async sendMessageGuarded(sessionName, message) {
165
310
  const session = this.getSessionOrThrow(sessionName);
166
311
  this.logger.debug('Sending message to session', {
167
312
  sessionName,
@@ -171,10 +316,14 @@ export class SessionCommandHelper {
171
316
  // Step 1: the box must be readable and empty before we type.
172
317
  let before = this.readInputBox(sessionName, message, 'before-write');
173
318
  if (before.ownPasteMarker) {
174
- // An earlier delivery of ours is still in the box (its Enter was
175
- // lost): submit it first, then deliver this one.
176
- this.logger.warn('An earlier paste of ours is still in the input box — submitting it before this message', { sessionName });
177
- await this.ensureOwnPasteSubmitted(sessionName);
319
+ // An earlier paste of ours is still in the box (its Enter was lost,
320
+ // or it rendered late): submit it first. When it is this very
321
+ // message, submitting it IS this delivery — never paste it twice.
322
+ const sameMessage = SessionCommandHelper.ownPastes.get(sessionName)?.message === message;
323
+ this.logger.warn('An earlier paste of ours is still in the input box — submitting it', { sessionName, sameMessage });
324
+ const outcome = await this.ensureOwnPasteSubmitted(sessionName);
325
+ if (sameMessage && outcome === 'submitted')
326
+ return;
178
327
  before = this.readInputBox(sessionName, message, 'before-write');
179
328
  }
180
329
  if (before.state === 'ours' && !before.ownPasteMarker) {
@@ -196,6 +345,7 @@ export class SessionCommandHelper {
196
345
  // Without this, special characters like (), $, `, and " can trigger
197
346
  // shell mode or be read as key sequences in Gemini CLI (#292, #293).
198
347
  session.write(`\x1b[200~${message}\x1b[201~`);
348
+ this.recordOwnPaste(sessionName, message);
199
349
  // Scale delay based on message size: large prompts (e.g. 409-line registration
200
350
  // prompts) need more time for Claude Code to process the bracketed paste.
201
351
  // Base delay + 1ms per 10 characters, capped at 5 seconds.
@@ -209,11 +359,13 @@ export class SessionCommandHelper {
209
359
  await delay(waitMs); // the paste may not have rendered yet
210
360
  after = this.readInputBox(sessionName, message, 'after-paste');
211
361
  }
212
- // Remember the collapsed marker only for callers that check the box
213
- // after delivery (sendMessageWithRetry → ensureOwnPasteSubmitted);
214
- // other paths leave no record that could outlive the paste.
215
- if (options.recordPasteMarker && after.state === 'ours' && isPasteMarker(after.text)) {
216
- SessionCommandHelper.ownPasteMarkers.set(sessionName, { marker: after.text.trim(), message, at: SessionCommandHelper.now() });
362
+ // A busy agent renders a paste late (Claude Code: ~10 s mid-turn,
363
+ // 2026-10-03 Ella). Keep looking a while before giving up; if it shows
364
+ // up later anyway, the record makes it ours and it is submitted then.
365
+ const renderDeadline = SessionCommandHelper.now() + SessionCommandHelper.pasteRenderMaxWaitMs;
366
+ while (after.state === 'empty' && SessionCommandHelper.now() < renderDeadline) {
367
+ await delay(TUI_INPUT_GUARD.PASTE_RENDER_POLL_MS);
368
+ after = this.readInputBox(sessionName, message, 'after-paste');
217
369
  }
218
370
  if (after.state !== 'ours') {
219
371
  this.logger.warn('Input box does not hold exactly our text after the paste — not pressing Enter, not clearing', {
@@ -260,7 +412,7 @@ export class SessionCommandHelper {
260
412
  const after = this.readInputBox(sessionName, '', 'recovery');
261
413
  // One Enter on it, whatever happened: never press Enter on that marker
262
414
  // again from this record.
263
- SessionCommandHelper.ownPasteMarkers.delete(sessionName);
415
+ SessionCommandHelper.forgetOwnPaste(sessionName);
264
416
  if (after.ownPasteMarker || (after.state === 'foreign' && after.text.trim() === reading.text.trim())) {
265
417
  this.logger.warn('Our pasted message is still in the input box after Enter — not delivered', { sessionName });
266
418
  return 'stuck';
@@ -272,7 +424,9 @@ export class SessionCommandHelper {
272
424
  * Forget recorded paste markers (tests).
273
425
  */
274
426
  static resetOwnPasteMarkersForTesting() {
275
- SessionCommandHelper.ownPasteMarkers.clear();
427
+ SessionCommandHelper.ownPastes.clear();
428
+ SessionCommandHelper.inFlight.clear();
429
+ SessionCommandHelper.stopWatchingIfIdle();
276
430
  }
277
431
  /**
278
432
  * Type a command line into a plain shell and run it (paste + Enter).
@@ -308,6 +462,9 @@ export class SessionCommandHelper {
308
462
  * @returns The reading
309
463
  */
310
464
  readInputBox(sessionName, message, stage = 'recovery') {
465
+ // First: a paste record that has gone unseen too long ends here, even
466
+ // when the box cannot be read now.
467
+ const shown = this.ownPasteShown(sessionName);
311
468
  const capture = this.backend.captureInputView;
312
469
  if (typeof capture !== 'function')
313
470
  return { state: 'unknown', text: '', lineCount: 0 };
@@ -315,19 +472,65 @@ export class SessionCommandHelper {
315
472
  const view = capture.call(this.backend, sessionName);
316
473
  if (!view)
317
474
  return { state: 'unknown', text: '', lineCount: 0 };
318
- // The marker of our own recent paste is ours whatever we send next —
319
- // and the record ends the moment the box shows anything else.
320
- const own = SessionCommandHelper.ownMarkerFor(sessionName);
321
- const reading = classifyTuiInput(view, message, stage, own);
322
- if (own && reading.state !== 'unknown' && !reading.ownPasteMarker) {
323
- SessionCommandHelper.ownPasteMarkers.delete(sessionName);
324
- }
325
- return reading;
475
+ return this.applyOwnPaste(sessionName, classifyTuiInput(view, message, stage, shown));
326
476
  }
327
477
  catch {
328
478
  return { state: 'unknown', text: '', lineCount: 0 };
329
479
  }
330
480
  }
481
+ /**
482
+ * What the box showed for our recorded paste, while the record stands.
483
+ * Ends the record when it has gone unseen too long.
484
+ *
485
+ * @param sessionName - The session
486
+ * @returns The text the box showed, or undefined (none seen / no record)
487
+ */
488
+ ownPasteShown(sessionName) {
489
+ const rec = SessionCommandHelper.ownPastes.get(sessionName);
490
+ if (!rec)
491
+ return undefined;
492
+ const now = SessionCommandHelper.now();
493
+ const unseenFor = now - (rec.lastSeenAt ?? rec.pastedAt);
494
+ const limit = rec.shown === undefined ? TUI_INPUT_GUARD.OWN_PASTE_PENDING_MAX_MS : TUI_INPUT_GUARD.OWN_PASTE_UNSEEN_MAX_MS;
495
+ if (unseenFor > limit) {
496
+ SessionCommandHelper.forgetOwnPaste(sessionName);
497
+ return undefined;
498
+ }
499
+ return rec.shown;
500
+ }
501
+ /**
502
+ * Keep the paste record in step with a reading: the record stands only
503
+ * while every readable box shows our paste. A pending paste (not seen
504
+ * yet) is adopted when the box first shows its shape; an empty box keeps
505
+ * it pending; anything else ends it.
506
+ *
507
+ * @param sessionName - The session
508
+ * @param reading - The classification just made
509
+ * @returns The reading, marked `ownPasteMarker` when the box shows our paste
510
+ */
511
+ applyOwnPaste(sessionName, reading) {
512
+ const rec = SessionCommandHelper.ownPastes.get(sessionName);
513
+ if (!rec || reading.state === 'unknown')
514
+ return reading;
515
+ const now = SessionCommandHelper.now();
516
+ if (rec.shown !== undefined) {
517
+ if (reading.ownPasteMarker) {
518
+ rec.lastSeenAt = now;
519
+ return reading;
520
+ }
521
+ SessionCommandHelper.forgetOwnPaste(sessionName);
522
+ return reading;
523
+ }
524
+ if (reading.state === 'empty')
525
+ return reading; // not rendered yet
526
+ if (pasteShowsAs(reading.text, rec.message)) {
527
+ rec.shown = reading.text.trim();
528
+ rec.lastSeenAt = now;
529
+ return { ...reading, state: 'ours', ownPasteMarker: true };
530
+ }
531
+ SessionCommandHelper.forgetOwnPaste(sessionName);
532
+ return reading;
533
+ }
331
534
  /**
332
535
  * Empty the input box — only call it for text proven to be the harness's
333
536
  * own. Sends Ctrl+U then Backspace (clear the line, join the empty line to