crewly 1.20.196 → 1.20.198

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 (234) hide show
  1. package/config/roles/orchestrator/prompt.md +8 -9
  2. package/config/roles/team-leader/prompt.md +6 -0
  3. package/config/skills/agent/computer-use/SKILL.md +16 -0
  4. package/config/skills/agent/core/ask-owner/SKILL.md +14 -0
  5. package/config/skills/agent/core/ask-owner/execute.sh +19 -1
  6. package/config/skills/agent/core/ask-owner/execute.test.sh +12 -0
  7. package/config/skills/agent/core/gmail-send/SKILL.md +16 -0
  8. package/config/skills/agent/core/reply-channel/SKILL.md +16 -0
  9. package/config/skills/agent/core/slack-post/SKILL.md +16 -0
  10. package/config/skills/agent/core/whatsapp-send/SKILL.md +16 -0
  11. package/config/skills/agent/remote-browser/SKILL.md +43 -7
  12. package/config/skills/registry.json +7 -7
  13. package/dist/backend/backend/src/constants.d.ts +143 -0
  14. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  15. package/dist/backend/backend/src/constants.js +159 -0
  16. package/dist/backend/backend/src/constants.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +12 -1
  18. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/browser/browser.controller.js +170 -9
  20. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/browser/browser.routes.js +5 -1
  23. package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
  25. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +37 -52
  26. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
  27. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/team/team.controller.js +13 -0
  29. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  30. package/dist/backend/backend/src/index.d.ts +17 -0
  31. package/dist/backend/backend/src/index.d.ts.map +1 -1
  32. package/dist/backend/backend/src/index.js +102 -0
  33. package/dist/backend/backend/src/index.js.map +1 -1
  34. package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.d.ts.map +1 -1
  35. package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.js +5 -8
  36. package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.js.map +1 -1
  37. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +12 -0
  38. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  39. package/dist/backend/backend/src/services/agent/agent-registration.service.js +275 -241
  40. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  41. package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/agent/context-window-monitor.service.js +5 -6
  43. package/dist/backend/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts +6 -0
  45. package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
  46. package/dist/backend/backend/src/services/agent/control-plane-guard.service.js +2 -1
  47. package/dist/backend/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
  48. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js +12 -8
  50. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
  51. package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -1
  52. package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +32 -27
  53. package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
  54. package/dist/backend/backend/src/services/agent/owner-stopped.registry.d.ts +44 -0
  55. package/dist/backend/backend/src/services/agent/owner-stopped.registry.d.ts.map +1 -0
  56. package/dist/backend/backend/src/services/agent/owner-stopped.registry.js +54 -0
  57. package/dist/backend/backend/src/services/agent/owner-stopped.registry.js.map +1 -0
  58. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +4 -4
  59. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  60. package/dist/backend/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/agent/runtime-exit-monitor.service.js +4 -8
  62. package/dist/backend/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -1
  63. package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +19 -4
  64. package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -1
  65. package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.js +28 -5
  66. package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -1
  67. package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts +2 -2
  68. package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts.map +1 -1
  69. package/dist/backend/backend/src/services/browser/browser-approval.service.js +13 -2
  70. package/dist/backend/backend/src/services/browser/browser-approval.service.js.map +1 -1
  71. package/dist/backend/backend/src/services/browser/browser-outbound-guard.d.ts +214 -0
  72. package/dist/backend/backend/src/services/browser/browser-outbound-guard.d.ts.map +1 -0
  73. package/dist/backend/backend/src/services/browser/browser-outbound-guard.js +466 -0
  74. package/dist/backend/backend/src/services/browser/browser-outbound-guard.js.map +1 -0
  75. package/dist/backend/backend/src/services/browser/browser-session.service.d.ts +57 -3
  76. package/dist/backend/backend/src/services/browser/browser-session.service.d.ts.map +1 -1
  77. package/dist/backend/backend/src/services/browser/browser-session.service.js +148 -63
  78. package/dist/backend/backend/src/services/browser/browser-session.service.js.map +1 -1
  79. package/dist/backend/backend/src/services/browser/held-action-store.d.ts +4 -0
  80. package/dist/backend/backend/src/services/browser/held-action-store.d.ts.map +1 -1
  81. package/dist/backend/backend/src/services/browser/held-action-store.js.map +1 -1
  82. package/dist/backend/backend/src/services/browser/owner-browser-input.d.ts +98 -10
  83. package/dist/backend/backend/src/services/browser/owner-browser-input.d.ts.map +1 -1
  84. package/dist/backend/backend/src/services/browser/owner-browser-input.js +126 -11
  85. package/dist/backend/backend/src/services/browser/owner-browser-input.js.map +1 -1
  86. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/cloud/cloud-initializer.js +6 -8
  88. package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
  89. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts +129 -0
  90. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -0
  91. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js +196 -0
  92. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -0
  93. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +42 -0
  94. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +88 -10
  96. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  97. package/dist/backend/backend/src/services/monitoring/agent-hook-state.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/monitoring/agent-hook-state.js +5 -0
  99. package/dist/backend/backend/src/services/monitoring/agent-hook-state.js.map +1 -1
  100. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
  101. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +5 -9
  102. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
  103. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +11 -0
  104. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  105. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +14 -0
  106. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  107. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.d.ts +37 -0
  108. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -1
  109. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.js +78 -0
  110. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -1
  111. package/dist/backend/backend/src/services/session/session-backend.interface.d.ts +13 -0
  112. package/dist/backend/backend/src/services/session/session-backend.interface.d.ts.map +1 -1
  113. package/dist/backend/backend/src/services/session/session-backend.interface.js.map +1 -1
  114. package/dist/backend/backend/src/services/session/session-command-helper.d.ts +118 -105
  115. package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  116. package/dist/backend/backend/src/services/session/session-command-helper.js +267 -298
  117. package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
  118. package/dist/backend/backend/src/services/session/tui-input-guard.d.ts +144 -0
  119. package/dist/backend/backend/src/services/session/tui-input-guard.d.ts.map +1 -0
  120. package/dist/backend/backend/src/services/session/tui-input-guard.js +312 -0
  121. package/dist/backend/backend/src/services/session/tui-input-guard.js.map +1 -0
  122. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  123. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +14 -0
  124. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  125. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +31 -0
  126. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  127. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +82 -4
  128. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  129. package/dist/backend/backend/src/services/trace/trace.types.d.ts +2 -2
  130. package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -1
  131. package/dist/backend/backend/src/services/trace/trace.types.js +4 -0
  132. package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -1
  133. package/dist/backend/backend/src/services/trace/turn-origin.d.ts +70 -0
  134. package/dist/backend/backend/src/services/trace/turn-origin.d.ts.map +1 -0
  135. package/dist/backend/backend/src/services/trace/turn-origin.js +198 -0
  136. package/dist/backend/backend/src/services/trace/turn-origin.js.map +1 -0
  137. package/dist/backend/backend/src/types/slack.types.d.ts +8 -0
  138. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  139. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  140. package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -1
  141. package/dist/backend/backend/src/utils/terminal-string-ops.js +3 -2
  142. package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -1
  143. package/dist/backend/build-info.json +2 -2
  144. package/dist/cli/backend/src/constants.d.ts +143 -0
  145. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  146. package/dist/cli/backend/src/constants.js +159 -0
  147. package/dist/cli/backend/src/constants.js.map +1 -1
  148. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +12 -0
  149. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  150. package/dist/cli/backend/src/services/agent/agent-registration.service.js +275 -241
  151. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  152. package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
  153. package/dist/cli/backend/src/services/agent/context-window-monitor.service.js +5 -6
  154. package/dist/cli/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
  155. package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts +6 -0
  156. package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
  157. package/dist/cli/backend/src/services/agent/control-plane-guard.service.js +2 -1
  158. package/dist/cli/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
  159. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
  160. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js +12 -8
  161. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
  162. package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -1
  163. package/dist/cli/backend/src/services/agent/gemini-runtime.service.js +32 -27
  164. package/dist/cli/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
  165. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +4 -4
  166. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  167. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -1
  168. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js +4 -8
  169. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -1
  170. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +19 -4
  171. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -1
  172. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js +28 -5
  173. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -1
  174. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts +129 -0
  175. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -0
  176. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js +196 -0
  177. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -0
  178. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +42 -0
  179. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  180. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +88 -10
  181. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  182. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts +11 -0
  183. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  184. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +14 -0
  185. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  186. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts +37 -0
  187. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -1
  188. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js +78 -0
  189. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -1
  190. package/dist/cli/backend/src/services/session/session-backend.interface.d.ts +13 -0
  191. package/dist/cli/backend/src/services/session/session-backend.interface.d.ts.map +1 -1
  192. package/dist/cli/backend/src/services/session/session-backend.interface.js.map +1 -1
  193. package/dist/cli/backend/src/services/session/session-command-helper.d.ts +118 -105
  194. package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  195. package/dist/cli/backend/src/services/session/session-command-helper.js +267 -298
  196. package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -1
  197. package/dist/cli/backend/src/services/session/tui-input-guard.d.ts +144 -0
  198. package/dist/cli/backend/src/services/session/tui-input-guard.d.ts.map +1 -0
  199. package/dist/cli/backend/src/services/session/tui-input-guard.js +312 -0
  200. package/dist/cli/backend/src/services/session/tui-input-guard.js.map +1 -0
  201. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  202. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +14 -0
  203. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  204. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +31 -0
  205. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  206. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +82 -4
  207. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  208. package/dist/cli/backend/src/services/trace/trace.types.d.ts +2 -2
  209. package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -1
  210. package/dist/cli/backend/src/services/trace/trace.types.js +4 -0
  211. package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -1
  212. package/dist/cli/backend/src/services/trace/turn-origin.d.ts +70 -0
  213. package/dist/cli/backend/src/services/trace/turn-origin.d.ts.map +1 -0
  214. package/dist/cli/backend/src/services/trace/turn-origin.js +198 -0
  215. package/dist/cli/backend/src/services/trace/turn-origin.js.map +1 -0
  216. package/dist/cli/backend/src/types/slack.types.d.ts +8 -0
  217. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  218. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  219. package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -1
  220. package/dist/cli/backend/src/utils/terminal-string-ops.js +3 -2
  221. package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -1
  222. package/frontend/dist/assets/index-5cbcf45b.css +42 -0
  223. package/frontend/dist/assets/{index-edfa8e04.js → index-63a33dbf.js} +283 -283
  224. package/frontend/dist/index.html +2 -2
  225. package/package.json +1 -1
  226. package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts +0 -118
  227. package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts.map +0 -1
  228. package/dist/cli/backend/src/services/agent/agent-suspend.service.js +0 -304
  229. package/dist/cli/backend/src/services/agent/agent-suspend.service.js.map +0 -1
  230. package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts +0 -100
  231. package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts.map +0 -1
  232. package/dist/cli/backend/src/services/agent/disk-cleanup.service.js +0 -149
  233. package/dist/cli/backend/src/services/agent/disk-cleanup.service.js.map +0 -1
  234. package/frontend/dist/assets/index-bfa0e2e8.css +0 -42
@@ -16,11 +16,13 @@
16
16
  * @module session-command-helper
17
17
  */
18
18
  import { LoggerService } from '../core/logger.service.js';
19
- import { SESSION_COMMAND_DELAYS, EVENT_DELIVERY_CONSTANTS, TERMINAL_PATTERNS, PLAN_MODE_DISMISS_PATTERNS } from '../../constants.js';
19
+ import { SESSION_COMMAND_DELAYS, EVENT_DELIVERY_CONSTANTS, PLAN_MODE_DISMISS_PATTERNS, TUI_INPUT_GUARD } from '../../constants.js';
20
20
  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';
25
+ import { noteHarnessWrite } from '../trace/turn-origin.js';
24
26
  /**
25
27
  * Key code mappings for special keys
26
28
  */
@@ -66,6 +68,43 @@ export const KEY_CODES = {
66
68
  export class SessionCommandHelper {
67
69
  logger;
68
70
  backend;
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). */
79
+ static now = Date.now;
80
+ /**
81
+ * Forget the recorded paste marker of a session: its runtime is being
82
+ * (re)started, a shell line is typed, or the session is created/killed —
83
+ * Claude Code's paste counter restarts at #1, and Codex markers carry only
84
+ * a character count, so an old record could match the owner's paste.
85
+ *
86
+ * @param sessionName - The session
87
+ */
88
+ static forgetOwnPaste(sessionName) {
89
+ SessionCommandHelper.ownPasteMarkers.delete(sessionName);
90
+ }
91
+ /**
92
+ * The recorded marker, while it may still be ours: at most
93
+ * OWN_MARKER_TTL_MS after our paste.
94
+ *
95
+ * @param sessionName - The session
96
+ * @returns The marker, or undefined
97
+ */
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;
105
+ }
106
+ return rec.marker;
107
+ }
69
108
  constructor(backend) {
70
109
  this.logger = LoggerService.getInstance().createComponentLogger('SessionCommandHelper');
71
110
  this.backend = backend;
@@ -97,46 +136,258 @@ export class SessionCommandHelper {
97
136
  return this.backend.getSession(sessionName);
98
137
  }
99
138
  /**
100
- * Send a message to a session with Enter key.
101
- * For multi-line messages, sends the text first then Enter separately
102
- * to avoid bracketed paste mode issues where Enter is treated as part of the paste.
139
+ * Send a message to an agent session and submit it with Enter — guarded.
140
+ *
141
+ * The harness only submits its own text (2026-10-03 phantom-input
142
+ * incident: a predicted "owner" message sitting in Claude Code's input
143
+ * was submitted with a delivery):
144
+ *
145
+ * 1. Before typing, the input box must be readable and empty. A leftover
146
+ * exact copy of this message (an earlier attempt) is cleared; anything
147
+ * else — someone's half-typed text, an unreadable screen — is left
148
+ * untouched and nothing is typed.
149
+ * 2. The message is pasted (bracketed paste) without Enter.
150
+ * 3. Enter is pressed only when the box holds exactly the message (or the
151
+ * runtime's collapsed "[Pasted text …]" marker). Otherwise nothing is
152
+ * submitted and nothing is cleared: text we cannot prove is ours is
153
+ * never wiped.
154
+ *
155
+ * A refusal throws `TuiInputGuardError`; agent delivery keeps the message
156
+ * queued and retries (AgentRegistrationService). Shell command lines
157
+ * typed before a runtime starts use {@link sendShellLine} instead.
103
158
  *
104
159
  * @param sessionName - The session to send to
105
160
  * @param message - The message to send
106
161
  * @throws Error if session does not exist
162
+ * @throws TuiInputGuardError when the box is unreadable or not ours
107
163
  */
108
- async sendMessage(sessionName, message) {
164
+ async sendMessage(sessionName, message, options = {}) {
109
165
  const session = this.getSessionOrThrow(sessionName);
110
166
  this.logger.debug('Sending message to session', {
111
167
  sessionName,
112
168
  messageLength: message.length,
113
169
  isMultiLine: message.includes('\n'),
114
170
  });
115
- // Wrap message in bracketed paste mode markers (\x1b[200~ ... \x1b[201~).
116
- // This tells TUI applications (Gemini CLI, Codex CLI, Claude Code) to treat
117
- // the input as pasted text rather than typed keystrokes. Without this, special
118
- // characters like (), $, `, and " can trigger shell mode or be interpreted as
119
- // key sequences in Gemini CLI (#292, #293).
120
- const bracketedMessage = `\x1b[200~${message}\x1b[201~`;
121
- // Send text first (in bracketed paste), then Enter separately to ensure submission
122
- session.write(bracketedMessage);
171
+ // Step 1: the box must be readable and empty before we type.
172
+ let before = this.readInputBox(sessionName, message, 'before-write');
173
+ 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);
178
+ before = this.readInputBox(sessionName, message, 'before-write');
179
+ }
180
+ if (before.state === 'ours' && !before.ownPasteMarker) {
181
+ // Our own earlier paste of this very message: safe to clear.
182
+ before = await this.clearInputBox(sessionName, message, before);
183
+ }
184
+ if (before.state !== 'empty') {
185
+ this.logger.warn('Not typing into the input box: it is unreadable or holds text the harness did not write', {
186
+ sessionName,
187
+ state: before.state,
188
+ layout: before.layout,
189
+ inputPreview: before.text.slice(0, 80),
190
+ });
191
+ throw new TuiInputGuardError('before-write', before);
192
+ }
193
+ // Step 2: paste. Wrap the message in bracketed paste markers
194
+ // (\x1b[200~ ... \x1b[201~) so TUI applications (Gemini CLI, Codex CLI,
195
+ // Claude Code) treat it as pasted text rather than typed keystrokes.
196
+ // Without this, special characters like (), $, `, and " can trigger
197
+ // shell mode or be read as key sequences in Gemini CLI (#292, #293).
198
+ session.write(`\x1b[200~${message}\x1b[201~`);
123
199
  // Scale delay based on message size: large prompts (e.g. 409-line registration
124
200
  // prompts) need more time for Claude Code to process the bracketed paste.
125
201
  // Base delay + 1ms per 10 characters, capped at 5 seconds.
126
202
  const scaledDelay = Math.min(SESSION_COMMAND_DELAYS.MESSAGE_DELAY + Math.ceil(message.length / 10), 5000);
127
203
  await delay(scaledDelay);
204
+ // Step 3: submit only our own text.
205
+ let after = this.readInputBox(sessionName, message, 'after-paste');
206
+ for (const waitMs of TUI_INPUT_GUARD.PASTE_RENDER_RETRY_MS) {
207
+ if (after.state !== 'empty')
208
+ break;
209
+ await delay(waitMs); // the paste may not have rendered yet
210
+ after = this.readInputBox(sessionName, message, 'after-paste');
211
+ }
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() });
217
+ }
218
+ if (after.state !== 'ours') {
219
+ this.logger.warn('Input box does not hold exactly our text after the paste — not pressing Enter, not clearing', {
220
+ sessionName,
221
+ state: after.state,
222
+ layout: after.layout,
223
+ inputPreview: after.text.slice(0, 80),
224
+ });
225
+ throw new TuiInputGuardError('before-submit', after);
226
+ }
227
+ // The prompt this Enter submits is a harness delivery: a submitted
228
+ // prompt with no such write before it is recorded as unsolicited.
229
+ noteHarnessWrite(sessionName);
128
230
  // Send Enter explicitly as a separate keystroke
129
231
  // Use \r (carriage return) which is the standard Enter key in terminals
130
232
  session.write('\r');
131
233
  // Additional delay for Enter to be processed
132
234
  await delay(SESSION_COMMAND_DELAYS.KEY_DELAY);
133
- // Log that we finished sending
134
235
  this.logger.debug('Message sent with Enter key', {
135
236
  sessionName,
136
237
  messageLength: message.length,
137
238
  pasteDelay: scaledDelay,
239
+ layout: after.layout,
138
240
  });
139
241
  }
242
+ /**
243
+ * After a delivery: if the box still holds the collapsed marker of our
244
+ * own paste, the Enter was lost — press Enter once and look again. Runs
245
+ * whatever the caller's success checks said: a fast-reply or weak-signal
246
+ * check can report "delivered" while the marker is still sitting there.
247
+ *
248
+ * @param sessionName - The session
249
+ * @returns `clear` (no marker of ours in the box), `submitted` (Enter took
250
+ * it), or `stuck` (still there after one Enter — not delivered)
251
+ */
252
+ async ensureOwnPasteSubmitted(sessionName) {
253
+ const reading = this.readInputBox(sessionName, '', 'recovery');
254
+ if (!reading.ownPasteMarker)
255
+ return 'clear';
256
+ const session = this.getSessionOrThrow(sessionName);
257
+ noteHarnessWrite(sessionName);
258
+ session.write('\r');
259
+ await delay(TUI_INPUT_GUARD.OWN_MARKER_SUBMIT_SETTLE_MS);
260
+ const after = this.readInputBox(sessionName, '', 'recovery');
261
+ // One Enter on it, whatever happened: never press Enter on that marker
262
+ // again from this record.
263
+ SessionCommandHelper.ownPasteMarkers.delete(sessionName);
264
+ if (after.ownPasteMarker || (after.state === 'foreign' && after.text.trim() === reading.text.trim())) {
265
+ this.logger.warn('Our pasted message is still in the input box after Enter — not delivered', { sessionName });
266
+ return 'stuck';
267
+ }
268
+ this.logger.info('Submitted our own paste whose Enter had been lost', { sessionName });
269
+ return 'submitted';
270
+ }
271
+ /**
272
+ * Forget recorded paste markers (tests).
273
+ */
274
+ static resetOwnPasteMarkersForTesting() {
275
+ SessionCommandHelper.ownPasteMarkers.clear();
276
+ }
277
+ /**
278
+ * Type a command line into a plain shell and run it (paste + Enter).
279
+ *
280
+ * Only for the lines Crewly types into a session's login shell before a
281
+ * runtime starts there (history off, `cd`, PATH, the launch command). It
282
+ * does not read an input box: a shell has none. Never use it to talk to
283
+ * an agent — that is {@link sendMessage}.
284
+ *
285
+ * @param sessionName - The session
286
+ * @param line - The command line
287
+ */
288
+ async sendShellLine(sessionName, line) {
289
+ const session = this.getSessionOrThrow(sessionName);
290
+ // A shell line means the runtime is (re)starting in this session.
291
+ SessionCommandHelper.forgetOwnPaste(sessionName);
292
+ session.write(`\x1b[200~${line}\x1b[201~`);
293
+ await delay(Math.min(SESSION_COMMAND_DELAYS.MESSAGE_DELAY + Math.ceil(line.length / 10), 5000));
294
+ session.write('\r');
295
+ await delay(SESSION_COMMAND_DELAYS.KEY_DELAY);
296
+ }
297
+ /**
298
+ * Read what an agent's input box really holds, relative to a message.
299
+ *
300
+ * Uses the backend's faint-free screen view, so ghost text (Claude Code's
301
+ * prompt suggestion, placeholders) reads as an empty box. Returns
302
+ * `unknown` when the backend has no styled capture or no input box of a
303
+ * known layout is on screen.
304
+ *
305
+ * @param sessionName - The session to read
306
+ * @param message - The harness's message to compare against ('' for none)
307
+ * @param stage - Why it is read (see classifyTuiInput)
308
+ * @returns The reading
309
+ */
310
+ readInputBox(sessionName, message, stage = 'recovery') {
311
+ const capture = this.backend.captureInputView;
312
+ if (typeof capture !== 'function')
313
+ return { state: 'unknown', text: '', lineCount: 0 };
314
+ try {
315
+ const view = capture.call(this.backend, sessionName);
316
+ if (!view)
317
+ 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;
326
+ }
327
+ catch {
328
+ return { state: 'unknown', text: '', lineCount: 0 };
329
+ }
330
+ }
331
+ /**
332
+ * Empty the input box — only call it for text proven to be the harness's
333
+ * own. Sends Ctrl+U then Backspace (clear the line, join the empty line to
334
+ * the one above), re-reading after each pair, until the box reads empty;
335
+ * one pair per line plus a margin (verified live on Claude Code, Codex
336
+ * and Gemini). A box that stops being readable is not "cleared": the
337
+ * final reading is then `unknown`, and callers treat only `empty` as
338
+ * success. Never sends Escape (cancels a running Claude Code turn; twice
339
+ * opens Rewind) or Ctrl+C (twice exits).
340
+ *
341
+ * @param sessionName - The session
342
+ * @param message - The harness's message, for classification
343
+ * @param current - A reading just taken (saves one re-read)
344
+ * @returns The final reading (`empty` when cleared)
345
+ */
346
+ async clearInputBox(sessionName, message, current) {
347
+ const session = this.getSessionOrThrow(sessionName);
348
+ let reading = current ?? this.readInputBox(sessionName, message, 'before-write');
349
+ if (reading.state === 'empty' || reading.state === 'unknown')
350
+ return reading;
351
+ const lines = Math.max(reading.lineCount, message.split('\n').length, 1);
352
+ const budget = Math.min(lines + TUI_INPUT_GUARD.CLEAR_PAIRS_EXTRA, TUI_INPUT_GUARD.CLEAR_PAIRS_MAX);
353
+ for (let i = 0; i < budget; i++) {
354
+ session.write(TUI_INPUT_GUARD.CLEAR_KEY);
355
+ session.write(TUI_INPUT_GUARD.JOIN_KEY);
356
+ await delay(TUI_INPUT_GUARD.CLEAR_SETTLE_MS);
357
+ reading = this.readInputBox(sessionName, message, 'before-write');
358
+ if (reading.state === 'empty' || reading.state === 'unknown')
359
+ break;
360
+ }
361
+ if (reading.state !== 'empty') {
362
+ this.logger.warn('Input box did not clear', { sessionName, state: reading.state, budget });
363
+ }
364
+ return reading;
365
+ }
366
+ /**
367
+ * Press Enter only when the input box holds exactly the harness's text.
368
+ * The safe replacement for "press Enter / Tab+Enter to recover a stuck
369
+ * message": an empty box (which may show a ghost suggestion) or a box
370
+ * with anyone else's text is never submitted.
371
+ *
372
+ * @param sessionName - The session
373
+ * @param message - The message the harness wrote
374
+ * @returns The reading; Enter was pressed only when its state is `ours`
375
+ */
376
+ async submitIfInputIsOurs(sessionName, message) {
377
+ const reading = this.readInputBox(sessionName, message, 'recovery');
378
+ if (reading.state !== 'ours') {
379
+ this.logger.debug('Not pressing Enter — input box does not hold our text', {
380
+ sessionName,
381
+ state: reading.state,
382
+ });
383
+ return reading;
384
+ }
385
+ const session = this.getSessionOrThrow(sessionName);
386
+ noteHarnessWrite(sessionName);
387
+ session.write('\r');
388
+ await delay(SESSION_COMMAND_DELAYS.KEY_DELAY);
389
+ return reading;
390
+ }
140
391
  /**
141
392
  * Send a key to a session
142
393
  *
@@ -188,61 +439,6 @@ export class SessionCommandHelper {
188
439
  this.logger.debug('Sent Escape to session', { sessionName });
189
440
  await delay(SESSION_COMMAND_DELAYS.KEY_DELAY);
190
441
  }
191
- /**
192
- * Gemini-specific message delivery (#130).
193
- *
194
- * Handles Ink TUI quirks:
195
- * 1. Sends Escape to exit sub-modes (shell mode, error overlay)
196
- * 2. Writes the message text
197
- * 3. Uses a longer delay before Enter (Ink TUI needs processing time)
198
- * 4. Verifies the message text left the input area (accepted)
199
- *
200
- * @param sessionName - The session to send to
201
- * @param message - The message to send
202
- * @returns true if post-write verification shows message was accepted
203
- */
204
- async sendMessageGemini(sessionName, message) {
205
- const session = this.getSessionOrThrow(sessionName);
206
- this.logger.debug('Gemini-specific message delivery', {
207
- sessionName,
208
- messageLength: message.length,
209
- });
210
- // Step 1: Escape to ensure we're not in a sub-mode
211
- session.write('\x1b');
212
- await delay(200);
213
- // Step 2: Write the message text wrapped in bracketed paste markers.
214
- // This prevents special characters from triggering shell mode (#292, #293).
215
- session.write(`\x1b[200~${message}\x1b[201~`);
216
- // Step 3: Longer delay for Ink TUI to process the paste
217
- const scaledDelay = Math.min(SESSION_COMMAND_DELAYS.MESSAGE_DELAY + Math.ceil(message.length / 10), 5000);
218
- await delay(scaledDelay);
219
- // Step 4: Send Enter with extra delay for Ink TUI
220
- session.write('\r');
221
- await delay(SESSION_COMMAND_DELAYS.KEY_DELAY * 2);
222
- // Step 5: Verify — check if message text disappeared from input area
223
- try {
224
- const postOutput = this.capturePane(sessionName, 10);
225
- const msgSnippet = (message.length > 30
226
- ? message.substring(0, 60)
227
- : message).replace(/\s+/g, ' ').trim();
228
- const bottomLines = postOutput.split('\n').slice(-5).join(' ').replace(/\s+/g, ' ');
229
- const textStillPresent = bottomLines.includes(msgSnippet);
230
- if (textStillPresent) {
231
- this.logger.warn('Gemini message text still in input after send — Enter may have been dropped', {
232
- sessionName,
233
- });
234
- return false;
235
- }
236
- this.logger.debug('Gemini message delivery verified — text accepted', {
237
- sessionName,
238
- });
239
- return true;
240
- }
241
- catch {
242
- // Verification failed, but message may have been delivered
243
- return true;
244
- }
245
- }
246
442
  /**
247
443
  * Detect and dismiss interactive prompts (e.g., plan mode) before message delivery.
248
444
  * Checks the last terminal output for plan mode patterns and sends Escape to exit
@@ -341,6 +537,7 @@ export class SessionCommandHelper {
341
537
  * Kill a session
342
538
  */
343
539
  async killSession(sessionName) {
540
+ SessionCommandHelper.forgetOwnPaste(sessionName);
344
541
  await this.backend.killSession(sessionName);
345
542
  this.logger.info('Session killed', { sessionName });
346
543
  }
@@ -354,6 +551,7 @@ export class SessionCommandHelper {
354
551
  */
355
552
  async createSession(sessionName, cwd, options) {
356
553
  this.logger.info('Creating session', { sessionName, cwd });
554
+ SessionCommandHelper.forgetOwnPaste(sessionName);
357
555
  // Default to shell if no command specified
358
556
  const command = options?.command || process.env.SHELL || '/bin/bash';
359
557
  const session = await this.backend.createSession(sessionName, {
@@ -567,235 +765,6 @@ export class SessionCommandHelper {
567
765
  });
568
766
  });
569
767
  }
570
- /**
571
- * Send message and wait for delivery confirmation via events.
572
- *
573
- * This is the core event-driven message delivery method. It:
574
- * 1. Sends the message to the terminal
575
- * 2. Subscribes to output to detect confirmation patterns
576
- * 3. Resolves when confirmation is detected or timeout occurs
577
- *
578
- * @param sessionName - The session to send to
579
- * @param message - The message to send
580
- * @param confirmationPattern - Pattern indicating successful delivery
581
- * @param timeoutMs - Maximum time to wait for confirmation
582
- * @returns Promise resolving to true if confirmed, false if timeout
583
- *
584
- * @example
585
- * ```typescript
586
- * // Send message and wait for processing indicator
587
- * const confirmed = await helper.sendMessageWithConfirmation(
588
- * 'agent-1',
589
- * 'Hello Claude',
590
- * /⠋|⠙|⠹|Thinking/,
591
- * 5000
592
- * );
593
- * ```
594
- */
595
- async sendMessageWithConfirmation(sessionName, message, confirmationPattern, timeoutMs = EVENT_DELIVERY_CONSTANTS.DELIVERY_CONFIRMATION_TIMEOUT) {
596
- const session = this.getSessionOrThrow(sessionName);
597
- return new Promise((resolve) => {
598
- let confirmed = false;
599
- let resolved = false;
600
- const cleanup = () => {
601
- if (!resolved) {
602
- resolved = true;
603
- clearTimeout(timeoutId);
604
- unsubscribe();
605
- }
606
- };
607
- const timeoutId = setTimeout(() => {
608
- this.logger.debug('sendMessageWithConfirmation timed out', {
609
- sessionName,
610
- messageLength: message.length,
611
- confirmed,
612
- });
613
- cleanup();
614
- resolve(confirmed);
615
- }, timeoutMs);
616
- const unsubscribe = session.onData((data) => {
617
- if (resolved)
618
- return;
619
- const match = typeof confirmationPattern === 'string'
620
- ? data.includes(confirmationPattern)
621
- : confirmationPattern.test(data);
622
- if (match) {
623
- this.logger.debug('Message delivery confirmed', {
624
- sessionName,
625
- pattern: confirmationPattern.toString(),
626
- });
627
- confirmed = true;
628
- cleanup();
629
- resolve(true);
630
- }
631
- });
632
- // Send the message
633
- this.logger.debug('Sending message with confirmation tracking', {
634
- sessionName,
635
- messageLength: message.length,
636
- });
637
- session.write(message);
638
- // Send Enter after a brief delay (for bracketed paste mode)
639
- setTimeout(() => {
640
- if (!resolved) {
641
- session.write('\r');
642
- }
643
- }, SESSION_COMMAND_DELAYS.MESSAGE_DELAY);
644
- });
645
- }
646
- /**
647
- * Smart message sending with event-driven paste detection.
648
- *
649
- * This method intelligently handles bracketed paste mode by:
650
- * 1. Sending the message text
651
- * 2. Waiting for "[Pasted text" indicator (paste received)
652
- * 3. Sending Enter key only after paste is confirmed
653
- * 4. Optionally waiting for processing indicators
654
- *
655
- * This is much more reliable than fixed delays because it detects
656
- * the actual terminal state rather than guessing timing.
657
- *
658
- * @param sessionName - The session to send to
659
- * @param message - The message to send
660
- * @param options - Configuration options
661
- * @returns Promise with delivery result
662
- *
663
- * @example
664
- * ```typescript
665
- * const result = await helper.sendMessageSmart('agent-1', 'Hello', {
666
- * waitForProcessing: true,
667
- * pasteTimeout: 3000,
668
- * });
669
- * if (result.pasteDetected && result.processingStarted) {
670
- * console.log('Message delivered and being processed');
671
- * }
672
- * ```
673
- */
674
- async sendMessageSmart(sessionName, message, options = {}) {
675
- const session = this.getSessionOrThrow(sessionName);
676
- const { pasteTimeout = 3000, processingTimeout = 5000, waitForProcessing = false, pastePattern = TERMINAL_PATTERNS.PASTE_INDICATOR, processingPattern = TERMINAL_PATTERNS.PROCESSING, fallbackDelay = 1500, } = options;
677
- const result = {
678
- pasteDetected: false,
679
- enterSent: false,
680
- processingStarted: false,
681
- usedFallback: false,
682
- };
683
- this.logger.debug('Smart message send starting', {
684
- sessionName,
685
- messageLength: message.length,
686
- isMultiLine: message.includes('\n'),
687
- });
688
- // Step 1: Send the message text (without Enter)
689
- session.write(message);
690
- // Step 2: Wait for paste detection or fallback
691
- try {
692
- await this.waitForPattern(sessionName, pastePattern, pasteTimeout);
693
- result.pasteDetected = true;
694
- this.logger.debug('Paste detected via pattern', { sessionName });
695
- }
696
- catch {
697
- // Paste pattern not detected within timeout - use fallback delay
698
- this.logger.debug('Paste pattern not detected, using fallback delay', {
699
- sessionName,
700
- fallbackDelay,
701
- });
702
- await delay(fallbackDelay);
703
- result.usedFallback = true;
704
- }
705
- // Step 3: Send Enter key
706
- session.write('\r');
707
- result.enterSent = true;
708
- this.logger.debug('Enter key sent', { sessionName });
709
- // Step 4: Optionally wait for processing indicators
710
- if (waitForProcessing) {
711
- try {
712
- await this.waitForPattern(sessionName, processingPattern, processingTimeout);
713
- result.processingStarted = true;
714
- this.logger.debug('Processing started', { sessionName });
715
- }
716
- catch {
717
- this.logger.debug('Processing pattern not detected within timeout', {
718
- sessionName,
719
- });
720
- }
721
- }
722
- this.logger.debug('Smart message send complete', {
723
- sessionName,
724
- result,
725
- });
726
- return result;
727
- }
728
- /**
729
- * Send message with retry logic using smart detection.
730
- *
731
- * Attempts to deliver a message with intelligent retry behavior:
732
- * - Uses event-driven paste detection
733
- * - Retries with Enter key if message appears stuck
734
- * - Detects common failure modes (prompt still visible, no response)
735
- *
736
- * @param sessionName - The session to send to
737
- * @param message - The message to send
738
- * @param maxRetries - Maximum number of retry attempts
739
- * @returns Promise resolving to true if message was successfully processed
740
- */
741
- async sendMessageWithSmartRetry(sessionName, message, maxRetries = 3) {
742
- const session = this.getSessionOrThrow(sessionName);
743
- // Dismiss plan mode if detected before attempting message delivery
744
- await this.dismissInteractivePromptIfNeeded(sessionName);
745
- // Use centralized patterns for consistency
746
- const stuckPattern = TERMINAL_PATTERNS.PASTE_STUCK;
747
- const processingPattern = TERMINAL_PATTERNS.PROCESSING;
748
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
749
- this.logger.debug('Smart retry attempt', { sessionName, attempt, maxRetries });
750
- if (attempt === 1) {
751
- // First attempt: use smart send
752
- const result = await this.sendMessageSmart(sessionName, message, {
753
- waitForProcessing: true,
754
- pasteTimeout: 3000,
755
- processingTimeout: 5000,
756
- });
757
- if (result.processingStarted) {
758
- this.logger.debug('Message delivered on first attempt', { sessionName });
759
- return true;
760
- }
761
- }
762
- else {
763
- // Retry attempts: just send Enter and check
764
- this.logger.debug('Sending retry Enter', { sessionName, attempt });
765
- session.write('\r');
766
- }
767
- // Wait a moment and check terminal state
768
- await delay(1000);
769
- // Capture current output to check state
770
- const output = this.backend.captureOutput(sessionName, 10);
771
- // Check if stuck (paste indicator still visible)
772
- if (stuckPattern.test(output)) {
773
- this.logger.debug('Message appears stuck, will retry with Enter', {
774
- sessionName,
775
- attempt,
776
- });
777
- continue;
778
- }
779
- // Check if processing started
780
- if (processingPattern.test(output)) {
781
- this.logger.debug('Processing detected after retry', { sessionName, attempt });
782
- return true;
783
- }
784
- // Check if prompt appeared (message may have been processed already)
785
- if (output.includes('❯') && !output.includes('[Pasted text')) {
786
- this.logger.debug('Prompt visible without paste indicator - likely processed', {
787
- sessionName,
788
- attempt,
789
- });
790
- return true;
791
- }
792
- }
793
- this.logger.warn('Message delivery failed after all retries', {
794
- sessionName,
795
- maxRetries,
796
- });
797
- return false;
798
- }
799
768
  /**
800
769
  * Write raw data to session without Enter key.
801
770
  *