crewly 1.20.197 → 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 (226) 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 +118 -3
  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 +23 -2
  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 +110 -62
  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/cloud/cloud-initializer.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/cloud/cloud-initializer.js +6 -8
  84. package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
  85. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts +129 -0
  86. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -0
  87. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js +196 -0
  88. package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -0
  89. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +42 -0
  90. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +88 -10
  92. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  93. package/dist/backend/backend/src/services/monitoring/agent-hook-state.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/monitoring/agent-hook-state.js +5 -0
  95. package/dist/backend/backend/src/services/monitoring/agent-hook-state.js.map +1 -1
  96. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
  97. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +5 -9
  98. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
  99. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +11 -0
  100. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  101. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +14 -0
  102. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  103. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.d.ts +37 -0
  104. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -1
  105. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.js +78 -0
  106. package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -1
  107. package/dist/backend/backend/src/services/session/session-backend.interface.d.ts +13 -0
  108. package/dist/backend/backend/src/services/session/session-backend.interface.d.ts.map +1 -1
  109. package/dist/backend/backend/src/services/session/session-backend.interface.js.map +1 -1
  110. package/dist/backend/backend/src/services/session/session-command-helper.d.ts +118 -105
  111. package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  112. package/dist/backend/backend/src/services/session/session-command-helper.js +267 -298
  113. package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
  114. package/dist/backend/backend/src/services/session/tui-input-guard.d.ts +144 -0
  115. package/dist/backend/backend/src/services/session/tui-input-guard.d.ts.map +1 -0
  116. package/dist/backend/backend/src/services/session/tui-input-guard.js +312 -0
  117. package/dist/backend/backend/src/services/session/tui-input-guard.js.map +1 -0
  118. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  119. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +14 -0
  120. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  121. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +31 -0
  122. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  123. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +82 -4
  124. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  125. package/dist/backend/backend/src/services/trace/trace.types.d.ts +2 -2
  126. package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -1
  127. package/dist/backend/backend/src/services/trace/trace.types.js +4 -0
  128. package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -1
  129. package/dist/backend/backend/src/services/trace/turn-origin.d.ts +70 -0
  130. package/dist/backend/backend/src/services/trace/turn-origin.d.ts.map +1 -0
  131. package/dist/backend/backend/src/services/trace/turn-origin.js +198 -0
  132. package/dist/backend/backend/src/services/trace/turn-origin.js.map +1 -0
  133. package/dist/backend/backend/src/types/slack.types.d.ts +8 -0
  134. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  135. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  136. package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -1
  137. package/dist/backend/backend/src/utils/terminal-string-ops.js +3 -2
  138. package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -1
  139. package/dist/backend/build-info.json +2 -2
  140. package/dist/cli/backend/src/constants.d.ts +143 -0
  141. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  142. package/dist/cli/backend/src/constants.js +159 -0
  143. package/dist/cli/backend/src/constants.js.map +1 -1
  144. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +12 -0
  145. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  146. package/dist/cli/backend/src/services/agent/agent-registration.service.js +275 -241
  147. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  148. package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
  149. package/dist/cli/backend/src/services/agent/context-window-monitor.service.js +5 -6
  150. package/dist/cli/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
  151. package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts +6 -0
  152. package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
  153. package/dist/cli/backend/src/services/agent/control-plane-guard.service.js +2 -1
  154. package/dist/cli/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
  155. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
  156. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js +12 -8
  157. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
  158. package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -1
  159. package/dist/cli/backend/src/services/agent/gemini-runtime.service.js +32 -27
  160. package/dist/cli/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
  161. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +4 -4
  162. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  163. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -1
  164. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js +4 -8
  165. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -1
  166. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +19 -4
  167. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -1
  168. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js +28 -5
  169. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -1
  170. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts +129 -0
  171. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -0
  172. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js +196 -0
  173. package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -0
  174. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +42 -0
  175. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  176. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +88 -10
  177. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  178. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts +11 -0
  179. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  180. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +14 -0
  181. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  182. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts +37 -0
  183. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -1
  184. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js +78 -0
  185. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -1
  186. package/dist/cli/backend/src/services/session/session-backend.interface.d.ts +13 -0
  187. package/dist/cli/backend/src/services/session/session-backend.interface.d.ts.map +1 -1
  188. package/dist/cli/backend/src/services/session/session-backend.interface.js.map +1 -1
  189. package/dist/cli/backend/src/services/session/session-command-helper.d.ts +118 -105
  190. package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  191. package/dist/cli/backend/src/services/session/session-command-helper.js +267 -298
  192. package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -1
  193. package/dist/cli/backend/src/services/session/tui-input-guard.d.ts +144 -0
  194. package/dist/cli/backend/src/services/session/tui-input-guard.d.ts.map +1 -0
  195. package/dist/cli/backend/src/services/session/tui-input-guard.js +312 -0
  196. package/dist/cli/backend/src/services/session/tui-input-guard.js.map +1 -0
  197. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  198. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +14 -0
  199. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  200. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +31 -0
  201. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  202. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +82 -4
  203. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  204. package/dist/cli/backend/src/services/trace/trace.types.d.ts +2 -2
  205. package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -1
  206. package/dist/cli/backend/src/services/trace/trace.types.js +4 -0
  207. package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -1
  208. package/dist/cli/backend/src/services/trace/turn-origin.d.ts +70 -0
  209. package/dist/cli/backend/src/services/trace/turn-origin.d.ts.map +1 -0
  210. package/dist/cli/backend/src/services/trace/turn-origin.js +198 -0
  211. package/dist/cli/backend/src/services/trace/turn-origin.js.map +1 -0
  212. package/dist/cli/backend/src/types/slack.types.d.ts +8 -0
  213. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  214. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  215. package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -1
  216. package/dist/cli/backend/src/utils/terminal-string-ops.js +3 -2
  217. package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -1
  218. package/package.json +1 -1
  219. package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts +0 -118
  220. package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts.map +0 -1
  221. package/dist/cli/backend/src/services/agent/agent-suspend.service.js +0 -304
  222. package/dist/cli/backend/src/services/agent/agent-suspend.service.js.map +0 -1
  223. package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts +0 -100
  224. package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts.map +0 -1
  225. package/dist/cli/backend/src/services/agent/disk-cleanup.service.js +0 -149
  226. package/dist/cli/backend/src/services/agent/disk-cleanup.service.js.map +0 -1
@@ -11,7 +11,9 @@ import { RuntimeServiceFactory } from './runtime-service.factory.js';
11
11
  import { OrcReplyRouteService } from '../orc/orc-reply-route.service.js';
12
12
  import { registerInProcessRuntime, unregisterInProcessRuntime, } from './crewly-agent/in-process-runtime-registry.js';
13
13
  import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
14
- import { CREWLY_CONSTANTS, ENV_CONSTANTS, OWNER_AUTH_CONSTANTS, AGENT_TIMEOUTS, ORCHESTRATOR_SESSION_NAME, ORCHESTRATOR_ROLE, RUNTIME_TYPES, SESSION_COMMAND_DELAYS, SESSION_RECREATION_CONSTANTS, EVENT_DELIVERY_CONSTANTS, TERMINAL_PATTERNS, GEMINI_SHELL_MODE_CONSTANTS, GEMINI_STUCK_CONNECTIVITY_PATTERN, GEMINI_ERROR_STATE_CONSTANTS, REGISTRATION_DELIVERY_CONSTANTS, ORC_CONVERSATION_CONSTANTS, SLACK_THREAD_KEY_CONSTANTS, OPEN_ITEMS_CONSTANTS, SPEND_CAP_CONSTANTS, } from '../../constants.js';
14
+ import { CREWLY_CONSTANTS, ENV_CONSTANTS, OWNER_AUTH_CONSTANTS, AGENT_TIMEOUTS, ORCHESTRATOR_SESSION_NAME, ORCHESTRATOR_ROLE, RUNTIME_TYPES, SESSION_COMMAND_DELAYS, SESSION_RECREATION_CONSTANTS, EVENT_DELIVERY_CONSTANTS, TERMINAL_PATTERNS, GEMINI_SHELL_MODE_CONSTANTS, GEMINI_STUCK_CONNECTIVITY_PATTERN, GEMINI_ERROR_STATE_CONSTANTS, REGISTRATION_DELIVERY_CONSTANTS, ORC_CONVERSATION_CONSTANTS, SLACK_THREAD_KEY_CONSTANTS, OPEN_ITEMS_CONSTANTS, SPEND_CAP_CONSTANTS, RUNTIME_INPUT_SAFETY, TUI_INPUT_GUARD, } from '../../constants.js';
15
+ import { TuiInputGuardError } from '../session/tui-input-guard.js';
16
+ import { InputBlockedRetryService } from '../messaging/input-blocked-retry.service.js';
15
17
  import { extractSlackThreadKeys, formatSlackThreadKey } from '../slack/slack-thread-key.js';
16
18
  import { delay } from '../../utils/async.utils.js';
17
19
  import { buildRuntimeModelFlags } from '../../utils/runtime-model-flags.utils.js';
@@ -31,7 +33,6 @@ import { InFlightTurnTracker } from '../restart/in-flight-turn-tracker.service.j
31
33
  import { AgentTurnStateService } from '../monitoring/agent-turn-state.js';
32
34
  import { FreshTaskConversationService } from './fresh-task-conversation.service.js';
33
35
  import { RestartDrainService } from '../restart/restart-drain.service.js';
34
- import { AgentSuspendService } from './agent-suspend.service.js';
35
36
  import { PromptBuilderService, buildModuleConfigFromTeamMember, memberSkillsJson, } from '../ai/prompt-builder.service.js';
36
37
  import { PromptAssemblyService } from '../ai/prompt-modules/prompt-assembly.service.js';
37
38
  import { renderActiveWorkSection } from '../ai/prompt-modules/active-work.module.js';
@@ -157,6 +158,12 @@ export class AgentRegistrationService {
157
158
  // Per-session hash of last sent message to prevent duplicate writes (#128).
158
159
  // Key: sessionName, Value: { hash, sentAt }
159
160
  lastSentMessageHash = new Map();
161
+ // Last input-guard refusal per session during the current delivery: the
162
+ // box was unreadable or held text we did not write. Such a message is
163
+ // kept queued and retried, never dropped (2026-10-03 review).
164
+ lastGuardRefusal = new Map();
165
+ // Consecutive guard refusals per session, for escalation.
166
+ guardRefusalCount = new Map();
160
167
  // In-process Crewly Agent runtimes (sessionName → runtime instance).
161
168
  // Used for crewly-agent runtimeType agents that run without PTY sessions.
162
169
  inProcessRuntimes = new Map();
@@ -178,12 +185,14 @@ export class AgentRegistrationService {
178
185
  RuntimeExitMonitorService.getInstance().setOnExitDetectedCallback((sessionName) => {
179
186
  this.cancelPendingRegistration(sessionName);
180
187
  this.unregisterTuiSession(sessionName);
181
- // Preserve queued messages for suspended agents (they'll be delivered on rehydrate)
182
- // and during shutdown: runtimes exit because the backend is stopping,
183
- // and messages held back by the restart drain must survive it.
184
- if (!AgentSuspendService.getInstance().isSuspended(sessionName) &&
185
- !RestartDrainService.getInstance().isDeliveryPaused()) {
186
- SubAgentMessageQueue.getInstance().clear(sessionName);
188
+ // Queued messages are kept when the runtime exits (crewly#1014
189
+ // review #4): they are someone's undelivered words. They go out
190
+ // when the agent is started again (registration flushes the
191
+ // queue; an agent that went down with messages queued is woken
192
+ // for them), and the queue reports anything it drops.
193
+ const pending = SubAgentMessageQueue.getInstance().getQueueSize(sessionName);
194
+ if (pending > 0) {
195
+ this.logger.warn('Runtime exited with messages still queued — keeping them for its next start', { sessionName, pending });
187
196
  }
188
197
  InFlightTurnTracker.getInstance().markTurnComplete(sessionName, 'runtime exited');
189
198
  // Its hook state belonged to the process that just exited.
@@ -1279,8 +1288,16 @@ export class AgentRegistrationService {
1279
1288
  * Tries to reset the runtime session and start fresh
1280
1289
  */
1281
1290
  async tryCleanupAndReinit(sessionName, role, timeout, projectPath, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE, runtimeFlags, additionalAllowlistPaths) {
1282
- // Clear Commandline
1283
- await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
1291
+ // Clear the shell's command line before the launch command is typed —
1292
+ // only when no runtime input box is on screen (a plain shell), and with
1293
+ // Ctrl+U alone: never Ctrl+C into a live runtime, never wipe an agent
1294
+ // box the harness cannot prove is its own (crewly#1014 review #3).
1295
+ {
1296
+ const helper = await this.getSessionHelper();
1297
+ if (helper.readInputBox(sessionName, '', 'before-write').state === 'unknown') {
1298
+ await helper.sendKey(sessionName, 'C-u');
1299
+ }
1300
+ }
1284
1301
  // Conversation id: preset for Claude Code (--session-id), resumed on
1285
1302
  // restore (--resume / `codex resume`), discovered after launch for Codex.
1286
1303
  const effectiveFlags = runtimeFlags ? [...runtimeFlags] : [];
@@ -1347,15 +1364,9 @@ export class AgentRegistrationService {
1347
1364
  // Drain stale terminal escape sequences (e.g. DA1 [?1;2c) that may have
1348
1365
  // arrived during postInitialize commands, so they don't leak into the prompt input
1349
1366
  await delay(500);
1350
- // Clear any pending input after post-initialization.
1351
- // Claude Code: Ctrl+C + Ctrl+U (clearCurrentCommandLine) — standard cleanup.
1352
- // Gemini CLI: Skip cleanup entirely — the TUI just started with a clean
1353
- // prompt. Ctrl+C at an empty Gemini CLI prompt triggers /quit and exits
1354
- // the CLI. Escape defocuses the TUI. Ctrl+U is ignored. The delay(500)
1355
- // above is sufficient to drain stale escape sequences.
1356
- if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
1357
- await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
1358
- }
1367
+ // No blind Ctrl+C / Ctrl+U after post-initialization (crewly#1014):
1368
+ // the guarded writer types only into a box it reads as empty, so
1369
+ // stale input is refused, never wiped unseen.
1359
1370
  }
1360
1371
  catch (postInitError) {
1361
1372
  this.logger.warn('Post-initialization hook failed (non-fatal)', {
@@ -1909,14 +1920,7 @@ export class AgentRegistrationService {
1909
1920
  // Drain stale terminal escape sequences (e.g. DA1 [?1;2c) that may have
1910
1921
  // arrived during postInitialize commands, so they don't leak into the prompt input
1911
1922
  await delay(500);
1912
- // Claude Code: Ctrl+C + Ctrl+U to clear any stale input.
1913
- // Gemini CLI (Ink TUI): Do NOT send any cleanup keystrokes.
1914
- // Escape defocuses the Ink TUI input permanently. Ctrl+C at empty
1915
- // prompt triggers /quit. Ctrl+U is ignored. The delay above is
1916
- // sufficient to drain stale escape sequences.
1917
- if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
1918
- await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
1919
- }
1923
+ // No blind Ctrl+C / Ctrl+U (crewly#1014): see post-initialization above.
1920
1924
  }
1921
1925
  catch (postInitError) {
1922
1926
  this.logger.warn('Post-initialization hook failed after recreation (non-fatal)', {
@@ -2637,11 +2641,12 @@ Loop until done, blocked, or explicitly reassigned:
2637
2641
  maxRetries,
2638
2642
  });
2639
2643
  try {
2640
- // Step 1: Send Ctrl+C to clear any pending commands (skip on first attempt if Claude was just initialized)
2644
+ // Step 1 (crewly#1014): no blind Ctrl+C / Ctrl+U before the
2645
+ // registration prompt — the guarded writer refuses a box that is
2646
+ // not empty, and only our own leftover text is ever cleared.
2641
2647
  if (!skipInitialCleanup || attempt > 1) {
2642
- await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
2643
2648
  await delay(500);
2644
- this.logger.debug('Sent Ctrl+C to clear terminal state', {
2649
+ this.logger.debug('Settled before the registration prompt (no clearing keys)', {
2645
2650
  sessionName,
2646
2651
  attempt,
2647
2652
  });
@@ -2893,6 +2898,8 @@ Loop until done, blocked, or explicitly reassigned:
2893
2898
  return {
2894
2899
  ...harnessEnvForAgents(process.env, runtimeType),
2895
2900
  ...(account ? claudeAccountEnv(account) : {}),
2901
+ // No predicted "user" text in the input box (2026-10-03 incident).
2902
+ ...(runtimeType === RUNTIME_TYPES.CLAUDE_CODE ? RUNTIME_INPUT_SAFETY.CLAUDE_CODE_ENV : {}),
2896
2903
  [ENV_CONSTANTS.CREWLY_SESSION_NAME]: sessionName,
2897
2904
  // The agent's credential (#999): every skill sends it, and a request
2898
2905
  // carrying it is this agent — never the owner.
@@ -4079,7 +4086,45 @@ Loop until done, blocked, or explicitly reassigned:
4079
4086
  // a finished turn. After the write the echo makes it look busy.
4080
4087
  const turnTracker = InFlightTurnTracker.getInstance();
4081
4088
  turnTracker.settle(sessionName);
4089
+ this.lastGuardRefusal.delete(sessionName);
4082
4090
  const delivered = await this.sendMessageWithRetry(sessionName, message, maxDeliveryAttempts, runtimeType);
4091
+ const refusal = this.lastGuardRefusal.get(sessionName);
4092
+ if (delivered) {
4093
+ // Never re-send a confirmed delivery (the scanner's re-queue).
4094
+ for (const e of this.sentMessageTracker.get(sessionName) ?? [])
4095
+ if (e.message === message)
4096
+ e.confirmed = true;
4097
+ this.guardRefusalCount.delete(sessionName);
4098
+ InputBlockedRetryService.getInstance().noteDelivered(sessionName);
4099
+ }
4100
+ if (!delivered && refusal) {
4101
+ // The harness would not type into, or submit, the agent's input box
4102
+ // (unreadable, or holding text it did not write). Keep the message
4103
+ // queued — it is retried when the agent is idle — and say so.
4104
+ SubAgentMessageQueue.getInstance().enqueue(sessionName, message);
4105
+ // Retried on a timer with backoff, and the owner told if it stays blocked.
4106
+ InputBlockedRetryService.getInstance().noteRefusal(sessionName, {
4107
+ state: refusal.reading.state,
4108
+ inputLength: refusal.reading.text.length,
4109
+ message,
4110
+ });
4111
+ const count = (this.guardRefusalCount.get(sessionName) ?? 0) + 1;
4112
+ this.guardRefusalCount.set(sessionName, count);
4113
+ const log = count >= TUI_INPUT_GUARD.ESCALATE_AFTER_REFUSALS ? this.logger.error.bind(this.logger) : this.logger.warn.bind(this.logger);
4114
+ log('Message held: the agent input box is unreadable or holds text the harness did not write — queued for retry', {
4115
+ sessionName,
4116
+ stage: refusal.stage,
4117
+ state: refusal.reading.state,
4118
+ layout: refusal.reading.layout,
4119
+ inputPreview: refusal.reading.text.slice(0, 80),
4120
+ consecutiveRefusals: count,
4121
+ });
4122
+ return {
4123
+ success: true,
4124
+ queued: true,
4125
+ message: '[INPUT_NOT_OURS] Message queued: the agent\'s input box is unreadable or holds text the harness did not write. It will be retried.',
4126
+ };
4127
+ }
4083
4128
  if (delivered) {
4084
4129
  turnTracker.recordDelivery(sessionName, message, 'pty');
4085
4130
  noteTurnDelivery(sessionName, message, 'pty');
@@ -4294,6 +4339,37 @@ Loop until done, blocked, or explicitly reassigned:
4294
4339
  * @returns true if message was delivered successfully
4295
4340
  */
4296
4341
  async sendMessageWithRetry(sessionName, message, maxAttempts = 3, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
4342
+ const delivered = await this.sendMessageWithRetryAttempts(sessionName, message, maxAttempts, runtimeType);
4343
+ if (!delivered)
4344
+ return false;
4345
+ // Whatever the checks above concluded (a fast reply, a weak output
4346
+ // change), a collapsed paste of ours still in the box means its Enter
4347
+ // was lost: press Enter once and look again (crewly#1014 review #4).
4348
+ try {
4349
+ const outcome = await (await this.getSessionHelper()).ensureOwnPasteSubmitted(sessionName);
4350
+ if (outcome === 'stuck') {
4351
+ this.logger.warn('Delivery looked confirmed but our paste is still in the input box — not delivered', { sessionName });
4352
+ return false;
4353
+ }
4354
+ if (outcome === 'submitted') {
4355
+ this.logger.warn('Delivery looked confirmed but its Enter was lost — submitted it now', { sessionName });
4356
+ }
4357
+ }
4358
+ catch {
4359
+ // No session helper / session gone: the checks above stand.
4360
+ }
4361
+ return true;
4362
+ }
4363
+ /**
4364
+ * The delivery attempts behind {@link sendMessageWithRetry}.
4365
+ *
4366
+ * @param sessionName - Target session
4367
+ * @param message - Message to deliver
4368
+ * @param maxAttempts - Attempts before giving up
4369
+ * @param runtimeType - The session's runtime
4370
+ * @returns Whether the checks saw the message accepted
4371
+ */
4372
+ async sendMessageWithRetryAttempts(sessionName, message, maxAttempts = 3, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
4297
4373
  const sessionHelper = await this.getSessionHelper();
4298
4374
  const isClaudeCode = runtimeType === RUNTIME_TYPES.CLAUDE_CODE;
4299
4375
  const isCodexCli = runtimeType === RUNTIME_TYPES.CODEX_CLI;
@@ -4395,12 +4471,9 @@ Loop until done, blocked, or explicitly reassigned:
4395
4471
  // This may restore the internal focus state. Then Tab + Enter
4396
4472
  // for focus cycling and overlay dismissal.
4397
4473
  if (isClaudeCode) {
4398
- if (attempt > 1) {
4399
- // On retry: Ctrl+C to cancel stale input, then PTY resize to force
4400
- // TUI re-render (SIGWINCH), then Tab to cycle Ink focus.
4401
- await sessionHelper.sendCtrlC(sessionName);
4402
- await delay(300);
4403
- }
4474
+ // No Ctrl+C on retry (2026-10-03 review): it wipes whatever is in
4475
+ // the box — possibly the owner's half-typed text — and cancels a
4476
+ // running turn. The guarded write only types into an empty box.
4404
4477
  // PTY resize on ALL attempts to force SIGWINCH → Ink TUI re-render.
4405
4478
  // Claude Code's Ink TUI can lose internal input focus after stop hooks
4406
4479
  // (especially ones that error), state transitions, or idle periods.
@@ -4420,18 +4493,17 @@ Loop until done, blocked, or explicitly reassigned:
4420
4493
  }
4421
4494
  }
4422
4495
  catch { /* non-fatal */ }
4423
- await sessionHelper.sendKey(sessionName, 'Tab');
4424
- await delay(300);
4496
+ // No Tab here (2026-10-03): in an empty Claude Code input, Tab
4497
+ // ACCEPTS the faint prompt suggestion — a predicted user message —
4498
+ // which the paste below then got appended to and submitted as if
4499
+ // the owner had typed it. The resize above restores focus.
4425
4500
  }
4426
4501
  else if (isAntigravity) {
4427
4502
  // Antigravity CLI: Ctrl+U clears stale input (verified on agy
4428
4503
  // 1.2.11). No Tab/Enter nudges: Tab completes slash commands and
4429
4504
  // the binary labels its spinner "Generating... (Enter/Esc to
4430
4505
  // cancel)", so a stray Enter or Esc can cancel a running turn.
4431
- if (attempt > 1) {
4432
- await sessionHelper.sendKey(sessionName, 'C-u');
4433
- await delay(300);
4434
- }
4506
+ // No blind Ctrl+U: only text proven ours is ever cleared.
4435
4507
  }
4436
4508
  else if (isCodexCli) {
4437
4509
  // #246: Codex CLI TUI recovery — simpler than Gemini.
@@ -4449,9 +4521,7 @@ Loop until done, blocked, or explicitly reassigned:
4449
4521
  }
4450
4522
  }
4451
4523
  catch { /* non-fatal */ }
4452
- // Ctrl+U to clear any stale input text
4453
- await sessionHelper.sendKey(sessionName, 'C-u');
4454
- await delay(300);
4524
+ // No blind Ctrl+U: only text proven ours is ever cleared.
4455
4525
  }
4456
4526
  }
4457
4527
  else {
@@ -4512,25 +4582,11 @@ Loop until done, blocked, or explicitly reassigned:
4512
4582
  // F12 to close error details panel if open
4513
4583
  await sessionHelper.sendKey(sessionName, 'F12');
4514
4584
  await delay(300);
4515
- // Enter to dismiss any remaining overlay/notification
4516
- await sessionHelper.sendEnter(sessionName);
4517
- await delay(500);
4518
4585
  }
4519
- // Send Tab to cycle Ink focus. In Ink v6, Tab triggers
4520
- // focusNext() in FocusContext, which moves focus to the next
4521
- // focusable component (InputPrompt). This works even when the
4522
- // input is defocused because the Tab handler runs at the Ink
4523
- // framework level, not the component level.
4524
- // Send two Tabs to cycle through error/notification components
4525
- // that may be in the focus chain (#130).
4526
- await sessionHelper.sendKey(sessionName, 'Tab');
4527
- await delay(200);
4528
- await sessionHelper.sendKey(sessionName, 'Tab');
4529
- await delay(300);
4530
- // Then Enter to dismiss any notification overlay and ensure
4531
- // the input is engaged. Enter on an empty `> ` prompt is a
4532
- // safe no-op (just shows a new blank prompt line).
4533
- await sessionHelper.sendEnter(sessionName);
4586
+ // No Tab/Enter focus nudges (2026-10-03): the harness never presses
4587
+ // Tab or Enter unless its own text is in the input box. Tab can
4588
+ // accept a completion/suggestion and Enter submits whatever the box
4589
+ // holds. The resize above re-renders the TUI to restore focus.
4534
4590
  // Extra settling time after /compress or error state to let Ink TUI stabilize
4535
4591
  await delay((compressDetected || errorStateDetected) ? 1000 : 500);
4536
4592
  }
@@ -4547,14 +4603,15 @@ Loop until done, blocked, or explicitly reassigned:
4547
4603
  ];
4548
4604
  const inInteractiveMode = interactiveModePatterns.some(p => preOutput.includes(p));
4549
4605
  if (inInteractiveMode) {
4550
- this.logger.warn('Gemini CLI in interactive mode, using non-destructive recovery', {
4606
+ // No Tab/Enter: they would pick an entry in the picker. Skip
4607
+ // this attempt and look again; the input guard in sendMessage
4608
+ // refuses to type while the box is not an empty prompt.
4609
+ this.logger.warn('Gemini CLI in interactive mode, skipping this delivery attempt', {
4551
4610
  sessionName,
4552
4611
  detectedPattern: interactiveModePatterns.find(p => preOutput.includes(p)),
4553
4612
  });
4554
- await sessionHelper.sendKey(sessionName, 'Tab');
4555
- await delay(250);
4556
- await sessionHelper.sendEnter(sessionName);
4557
- await delay(1000);
4613
+ await delay(SESSION_COMMAND_DELAYS.MESSAGE_RETRY_DELAY);
4614
+ continue;
4558
4615
  }
4559
4616
  }
4560
4617
  // Capture output BEFORE sending to detect changes for ALL runtimes.
@@ -4651,7 +4708,8 @@ Loop until done, blocked, or explicitly reassigned:
4651
4708
  // 2. await delay(scaled) — waits for paste processing
4652
4709
  // 3. session.write('\r') — sends Enter separately
4653
4710
  // 4. await delay(KEY_DELAY) — waits for key processing
4654
- await sessionHelper.sendMessage(sessionName, message);
4711
+ // Recorded marker: this path checks the box after delivery.
4712
+ await sessionHelper.sendMessage(sessionName, message, { recordPasteMarker: true });
4655
4713
  // Register for background stuck-detection safety net (all runtimes).
4656
4714
  // If progressive verification below misses an Enter drop, the
4657
4715
  // background scanner will catch it within 30s.
@@ -4727,20 +4785,17 @@ Loop until done, blocked, or explicitly reassigned:
4727
4785
  : message).replace(/\s+/g, ' ').trim();
4728
4786
  const promptBottomLines = currentOutput.split('\n').slice(-10).join(' ').replace(/\s+/g, ' ');
4729
4787
  if (promptBottomLines.includes(promptMsgSnippet)) {
4730
- this.logger.warn('At prompt with message text at bottom — pressing Tab+Enter', {
4788
+ // Our text near the bottom may be the transcript echo of a
4789
+ // message already submitted. Press Enter only if the input
4790
+ // box itself holds exactly our text (never Tab: it accepts a
4791
+ // prompt suggestion — the 2026-10-03 phantom owner input).
4792
+ const pressed = (await sessionHelper.submitIfInputIsOurs(sessionName, message)).state === 'ours';
4793
+ this.logger.warn('At prompt with message text at bottom', {
4731
4794
  sessionName,
4732
4795
  attempt,
4733
4796
  intervalMs,
4797
+ enterPressed: pressed,
4734
4798
  });
4735
- // Tab restores Ink TUI focus before Enter — without this,
4736
- // Enter may be consumed by the framework but not routed to
4737
- // the input component if focus was lost during a re-render
4738
- // (e.g., after stop hooks, state transitions).
4739
- await sessionHelper.sendKey(sessionName, 'Tab');
4740
- await delay(200);
4741
- await sessionHelper.sendEnter(sessionName);
4742
- await delay(500);
4743
- await sessionHelper.sendEnter(sessionName); // backup
4744
4799
  await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
4745
4800
  // Verify recovery
4746
4801
  const postEnterOutput = sessionHelper.capturePane(sessionName);
@@ -4796,16 +4851,14 @@ Loop until done, blocked, or explicitly reassigned:
4796
4851
  // prompt is no longer in its idle form — Enter was dropped.
4797
4852
  // Instead of waiting and doing a full Ctrl+C + resend retry,
4798
4853
  // use Tab to restore TUI focus, then Enter to submit.
4799
- this.logger.warn('Message text stuck at bottom — pressing Tab+Enter to recover', {
4854
+ // Enter only when the input box holds exactly our text.
4855
+ const pressed = (await sessionHelper.submitIfInputIsOurs(sessionName, message)).state === 'ours';
4856
+ this.logger.warn('Message text stuck at bottom', {
4800
4857
  sessionName,
4801
4858
  attempt,
4802
4859
  intervalMs,
4860
+ enterPressed: pressed,
4803
4861
  });
4804
- await sessionHelper.sendKey(sessionName, 'Tab');
4805
- await delay(200);
4806
- await sessionHelper.sendEnter(sessionName);
4807
- await delay(500);
4808
- await sessionHelper.sendEnter(sessionName); // backup Enter
4809
4862
  // Give the agent a moment to start processing after Enter
4810
4863
  await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
4811
4864
  // Verify recovery: check if processing started
@@ -4849,7 +4902,7 @@ Loop until done, blocked, or explicitly reassigned:
4849
4902
  // If progressive verification didn't confirm delivery, run an
4850
4903
  // extended check loop (every 3s, up to 30s total). Each iteration
4851
4904
  // captures the pane, checks for spinner/working indicators, and
4852
- // presses Tab+Enter if message text is still stuck at the bottom.
4905
+ // presses Enter if our message text is still in the input box.
4853
4906
  if (!claudeDelivered) {
4854
4907
  const CONFIRM_INTERVAL_MS = 3000;
4855
4908
  const CONFIRM_MAX_MS = 30000;
@@ -4874,7 +4927,7 @@ Loop until done, blocked, or explicitly reassigned:
4874
4927
  break;
4875
4928
  }
4876
4929
  // Text still stuck → Enter was dropped, NOT delivered. Try
4877
- // Tab+Enter recovery and re-check next iteration. This is
4930
+ // Enter (only if the box holds our text) and re-check next iteration. This is
4878
4931
  // checked BEFORE the output-change shortcut below: when our
4879
4932
  // own pasted message is echoed at the prompt the output DID
4880
4933
  // change vs the pre-send baseline, but that change is the
@@ -4882,14 +4935,10 @@ Loop until done, blocked, or explicitly reassigned:
4882
4935
  // is the false-positive that masked the orc 假死 (#686
4883
4936
  // follow-up: a hung session looked "delivered" forever).
4884
4937
  if (textStuck) {
4885
- this.logger.warn('Confirmation loop: text still stuck, pressing Tab+Enter', {
4886
- sessionName, attempt, confirmAttempt,
4938
+ const pressed = (await sessionHelper.submitIfInputIsOurs(sessionName, message)).state === 'ours';
4939
+ this.logger.warn('Confirmation loop: text still stuck', {
4940
+ sessionName, attempt, confirmAttempt, enterPressed: pressed,
4887
4941
  });
4888
- await sessionHelper.sendKey(sessionName, 'Tab');
4889
- await delay(200);
4890
- await sessionHelper.sendEnter(sessionName);
4891
- await delay(500);
4892
- await sessionHelper.sendEnter(sessionName); // backup
4893
4942
  continue;
4894
4943
  }
4895
4944
  // Fast-response detection: output changed since send AND our
@@ -4942,8 +4991,9 @@ Loop until done, blocked, or explicitly reassigned:
4942
4991
  this.logger.debug('Antigravity: message left the prompt box — delivery trusted', { sessionName, attempt });
4943
4992
  return true;
4944
4993
  }
4945
- this.logger.warn('Antigravity: message still in the prompt box — pressing Enter once', { sessionName, attempt });
4946
- await sessionHelper.sendEnter(sessionName);
4994
+ // Enter only when the box holds exactly our message.
4995
+ const pressed = (await sessionHelper.submitIfInputIsOurs(sessionName, message)).state === 'ours';
4996
+ this.logger.warn('Antigravity: message still in the prompt box', { sessionName, attempt, enterPressed: pressed });
4947
4997
  await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
4948
4998
  if (!isTextInAntigravityInputBox(sessionHelper.capturePane(sessionName), message)) {
4949
4999
  return true;
@@ -4981,6 +5031,17 @@ Loop until done, blocked, or explicitly reassigned:
4981
5031
  }
4982
5032
  else {
4983
5033
  // --- Non-Gemini TUI runtimes: full verification ---
5034
+ // Phase 0: read the box. The guarded writer pressed Enter only
5035
+ // on our own text; a box that now reads empty in a verified
5036
+ // layout means it went in — the output-length heuristics below
5037
+ // miss that (Codex: "did not change" → a retry → a duplicate).
5038
+ {
5039
+ const boxAfter = sessionHelper.readInputBox(sessionName, message, 'recovery');
5040
+ if (boxAfter.state === 'empty' && boxAfter.verified) {
5041
+ this.logger.debug('Input box empty after our Enter — delivery confirmed', { sessionName, attempt, layout: boxAfter.layout });
5042
+ return true;
5043
+ }
5044
+ }
4984
5045
  // Phase 1: Direct stuck-at-prompt detection
4985
5046
  const stuckAtPrompt = this.isTextStuckAtTuiPrompt(sessionName, message);
4986
5047
  if (stuckAtPrompt) {
@@ -5048,28 +5109,13 @@ Loop until done, blocked, or explicitly reassigned:
5048
5109
  sessionName,
5049
5110
  attempt,
5050
5111
  });
5051
- if (isClaudeCode) {
5052
- await sessionHelper.clearCurrentCommandLine(sessionName);
5053
- }
5054
- else if (isAntigravity) {
5055
- // Antigravity: clear the prompt box (Ctrl+U) so the retry types
5056
- // the message into an empty box. No Ctrl+C (arms exit), no Tab.
5057
- await sessionHelper.sendKey(sessionName, 'C-u');
5058
- await delay(300);
5059
- }
5060
- else {
5061
- // Gemini CLI retry cleanup: NEVER send Ctrl+C — it triggers /quit
5062
- // and kills the CLI entirely, regardless of whether text is in the
5063
- // input box or not. Use Tab to cycle Ink focus back to the input
5064
- // prompt, then Enter to submit any pending text.
5065
- this.logger.warn('Gemini CLI message stuck — using Tab focus recovery (no Ctrl+C)', {
5066
- sessionName,
5067
- attempt,
5068
- });
5069
- await sessionHelper.sendKey(sessionName, 'Tab');
5070
- await delay(300);
5071
- await sessionHelper.sendEnter(sessionName);
5072
- await delay(300);
5112
+ // Clear the box only when it holds exactly our message (2026-10-03
5113
+ // review): never Ctrl+C (cancels a turn; twice exits Claude Code;
5114
+ // /quit in Gemini), never wipe text we cannot prove is ours.
5115
+ {
5116
+ const box = sessionHelper.readInputBox(sessionName, message, 'before-write');
5117
+ if (box.state === 'ours')
5118
+ await sessionHelper.clearInputBox(sessionName, message, box);
5073
5119
  }
5074
5120
  await delay(SESSION_COMMAND_DELAYS.CLEAR_COMMAND_DELAY);
5075
5121
  if (attempt < maxAttempts) {
@@ -5077,6 +5123,9 @@ Loop until done, blocked, or explicitly reassigned:
5077
5123
  }
5078
5124
  }
5079
5125
  catch (error) {
5126
+ if (error instanceof TuiInputGuardError) {
5127
+ this.lastGuardRefusal.set(sessionName, error);
5128
+ }
5080
5129
  this.logger.error('Error during message delivery', {
5081
5130
  sessionName,
5082
5131
  attempt,
@@ -5237,16 +5286,15 @@ Loop until done, blocked, or explicitly reassigned:
5237
5286
  */
5238
5287
  async recoverStuckTuiMessage(sessionName, message) {
5239
5288
  const sessionHelper = await this.getSessionHelper();
5240
- // Tab restores Ink TUI focus, then Enter to submit the stuck text.
5241
- // Without Tab, Enter may be consumed by the framework but not routed
5242
- // to the input component if focus was lost during a re-render.
5243
- this.logger.info('Pressing Tab+Enter to recover stuck TUI message', { sessionName });
5244
- await sessionHelper.sendKey(sessionName, 'Tab');
5245
- await delay(200);
5246
- await sessionHelper.sendEnter(sessionName);
5247
- await delay(500);
5248
- // Double-tap: send a backup Enter in case the first was consumed
5249
- await sessionHelper.sendEnter(sessionName);
5289
+ // Enter only when the input box holds exactly our text — never Tab
5290
+ // (accepts a suggestion) and no blind backup Enter (submits whatever
5291
+ // the box holds once ours is gone).
5292
+ const reading = await sessionHelper.submitIfInputIsOurs(sessionName, message);
5293
+ // Enter only for text read as ours — never on an unreadable box.
5294
+ const pressed = reading.state === 'ours';
5295
+ this.logger.info('Stuck TUI message: Enter only if the box holds our text', { sessionName, enterPressed: pressed, state: reading.state });
5296
+ if (!pressed)
5297
+ return false;
5250
5298
  await delay(2000);
5251
5299
  // Verify text is no longer at prompt
5252
5300
  const stillStuck = this.isTextStuckAtTuiPrompt(sessionName, message);
@@ -5327,90 +5375,22 @@ Loop until done, blocked, or explicitly reassigned:
5327
5375
  // Non-fatal: session may have been destroyed
5328
5376
  }
5329
5377
  }
5330
- // --- Part 1: Existing TUI prompt-line scanning ---
5331
- for (const [sessionName, runtimeType] of this.tuiSessionRegistry) {
5332
- try {
5333
- // Skip Gemini CLI sessions — their TUI prompt behaves differently
5334
- // and false-positive stuck detection causes Tab+Enter spam.
5335
- // Skip Antigravity too: its idle placeholder and the `> text` echo
5336
- // of every submitted message sit on `>` lines, and Enter during a
5337
- // turn can cancel it; its deliveries are verified on the prompt box.
5338
- // Skip Codex: its empty composer shows a rotating suggestion
5339
- // ("Ask Codex to do anything", "Explain this codebase", ...) on the
5340
- // `›` line, indistinguishable from typed text once the dim styling is
5341
- // lost, so every scan of a working Codex agent pressed Tab+Enter
5342
- // (47 times in one turn). Tab queues input in Codex; real Enter drops
5343
- // are still caught by Part 2, which matches the text actually sent.
5344
- if (runtimeType === RUNTIME_TYPES.GEMINI_CLI ||
5345
- runtimeType === RUNTIME_TYPES.ANTIGRAVITY_CLI ||
5346
- runtimeType === RUNTIME_TYPES.CODEX_CLI) {
5347
- continue;
5348
- }
5349
- // #213: Skip orchestrator — it frequently waits for long-running
5350
- // agent tasks (30+ min), and false stuck detection causes disruptive
5351
- // Tab+Enter key injections that interrupt its workflow.
5352
- if (sessionName === ORCHESTRATOR_SESSION_NAME) {
5353
- continue;
5354
- }
5355
- // Skip sessions that no longer exist
5356
- if (!sessionHelper.sessionExists(sessionName)) {
5357
- this.tuiSessionRegistry.delete(sessionName);
5358
- continue;
5359
- }
5360
- const output = sessionHelper.capturePane(sessionName);
5361
- if (!output || output.trim().length === 0)
5362
- continue;
5363
- // Look for any text sitting on the prompt line
5364
- const lines = output.split('\n');
5365
- for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
5366
- const promptContent = matchTuiPromptLine(lines[i]);
5367
- if (promptContent !== null) {
5368
- const trimmedContent = stripTuiLineBorders(promptContent).trim();
5369
- // Only act on substantial text (> 10 chars) to avoid false positives
5370
- // from TUI rendering artifacts or short status text
5371
- if (trimmedContent.length > 10) {
5372
- // Skip known idle placeholder text that sits at
5373
- // the `> ` prompt when no user input is present. These are
5374
- // NOT stuck messages — they are TUI decoration.
5375
- const lowerContent = trimmedContent.toLowerCase();
5376
- const isPlaceholder = lowerContent.startsWith('type your message') ||
5377
- trimmedContent.startsWith('@'); // e.g., "@path/to/file"
5378
- if (isPlaceholder) {
5379
- break;
5380
- }
5381
- this.logger.warn('Background scan: text stuck at TUI prompt, pressing Tab+Enter', {
5382
- sessionName,
5383
- promptContent: promptContent.slice(0, 80),
5384
- });
5385
- // Tab restores Ink TUI focus before Enter
5386
- await sessionHelper.sendKey(sessionName, 'Tab');
5387
- await delay(200);
5388
- await sessionHelper.sendEnter(sessionName);
5389
- await delay(500);
5390
- // Backup Enter
5391
- await sessionHelper.sendEnter(sessionName);
5392
- }
5393
- break; // Only check the first prompt line found from the bottom
5394
- }
5395
- }
5396
- }
5397
- catch (error) {
5398
- this.logger.debug('Error scanning session for stuck messages', {
5399
- sessionName,
5400
- error: error instanceof Error ? error.message : String(error),
5401
- });
5402
- }
5403
- }
5378
+ // --- Part 1 (removed 2026-10-03) ---
5379
+ // The old Part 1 pressed Tab+Enter on ANY text sitting on a TUI prompt
5380
+ // line — text the harness never wrote (a prompt suggestion accepted by
5381
+ // the Tab, or someone's half-typed input) got submitted. The harness
5382
+ // now only submits its own tracked messages (Part 2), and only when
5383
+ // the input box holds exactly that text.
5404
5384
  // --- Part 2: Tracked-message scanning (non-Gemini runtimes) ---
5405
5385
  const now = Date.now();
5406
5386
  const MIN_AGE_MS = 10000; // Check messages older than 10s
5407
- const MAX_RECOVERY_ATTEMPTS = 5; // Give up after 5 Tab+Enter attempts
5387
+ const MAX_RECOVERY_ATTEMPTS = 5; // Give up after 5 Enter attempts
5408
5388
  for (const [sessionName, entries] of this.sentMessageTracker) {
5409
5389
  if (!sessionHelper.sessionExists(sessionName)) {
5410
5390
  this.sentMessageTracker.delete(sessionName);
5411
5391
  continue;
5412
5392
  }
5413
- // Skip Gemini CLI sessions — Tab+Enter recovery causes more harm
5393
+ // Skip Gemini CLI sessions — Enter recovery causes more harm
5414
5394
  // than good because the Gemini TUI handles input differently.
5415
5395
  const trackedRuntime = this.tuiSessionRegistry.get(sessionName);
5416
5396
  if (trackedRuntime === RUNTIME_TYPES.GEMINI_CLI || trackedRuntime === RUNTIME_TYPES.ANTIGRAVITY_CLI) {
@@ -5427,9 +5407,33 @@ Loop until done, blocked, or explicitly reassigned:
5427
5407
  if (now - entry.sentAt < MIN_AGE_MS)
5428
5408
  continue;
5429
5409
  if (entry.recoveryAttempts >= MAX_RECOVERY_ATTEMPTS) {
5430
- // Exhausted recovery attempts — mark as recovered to stop retrying
5431
5410
  entry.recovered = true;
5432
- this.logger.error('Background scan: max recovery attempts exhausted', {
5411
+ const alreadyQueued = !!entry.message && SubAgentMessageQueue.getInstance().contains(sessionName, entry.message);
5412
+ if (entry.unreadable && entry.message && (entry.confirmed || alreadyQueued)) {
5413
+ // Delivery was confirmed earlier (or the message is queued
5414
+ // already): re-sending would duplicate it.
5415
+ this.logger.warn('Background scan: input box stayed unreadable — message was already delivered or queued, not re-sending', {
5416
+ sessionName,
5417
+ snippet: entry.snippet.slice(0, 50),
5418
+ confirmed: !!entry.confirmed,
5419
+ });
5420
+ continue;
5421
+ }
5422
+ if (entry.unreadable && entry.message) {
5423
+ // The box never became readable: we cannot tell whether
5424
+ // the message went in. Hand it back to the queue (retried
5425
+ // with backoff, the owner told if it stays blocked) rather
5426
+ // than give up on it (review #3 of crewly#1014).
5427
+ SubAgentMessageQueue.getInstance().enqueue(sessionName, entry.message);
5428
+ InputBlockedRetryService.getInstance().noteRefusal(sessionName, { state: 'unknown', inputLength: 0, message: entry.message });
5429
+ this.logger.error('Background scan: input box stayed unreadable — message re-queued', {
5430
+ sessionName,
5431
+ snippet: entry.snippet.slice(0, 50),
5432
+ attempts: entry.recoveryAttempts,
5433
+ });
5434
+ continue;
5435
+ }
5436
+ this.logger.error('Background scan: max recovery attempts exhausted — the message may not have been delivered', {
5433
5437
  sessionName,
5434
5438
  snippet: entry.snippet.slice(0, 50),
5435
5439
  attempts: entry.recoveryAttempts,
@@ -5437,20 +5441,74 @@ Loop until done, blocked, or explicitly reassigned:
5437
5441
  continue;
5438
5442
  }
5439
5443
  if (bottomText.includes(entry.snippet)) {
5444
+ // The text near the bottom is often only the transcript echo
5445
+ // of a message already submitted. Enter is pressed only when
5446
+ // the input box itself holds exactly this message — never on
5447
+ // an empty box (it may show a ghost prompt suggestion; Enter
5448
+ // or Tab would submit it as the owner — 2026-10-03).
5449
+ const reading = await sessionHelper.submitIfInputIsOurs(sessionName, entry.message);
5450
+ if (reading.state === 'empty') {
5451
+ // The box is empty: our message was submitted (the text
5452
+ // seen was its echo). Nothing to do.
5453
+ entry.recovered = true;
5454
+ continue;
5455
+ }
5456
+ if (reading.state === 'foreign') {
5457
+ const squashed = (t) => t.replace(/\s+/g, '');
5458
+ if (squashed(reading.text).includes(squashed(entry.snippet))) {
5459
+ // Our message is in the box mixed with text we did not
5460
+ // write. Never wipe text we cannot prove is ours: leave the
5461
+ // box as it is and hand the message back to delivery, which
5462
+ // keeps it queued until the box is empty. The entry is marked
5463
+ // recovered only once delivery has it (2026-10-03 review).
5464
+ this.logger.warn('Background scan: tracked message stuck with foreign text in the box — not touching it, re-queuing the message', {
5465
+ sessionName,
5466
+ snippet: entry.snippet.slice(0, 50),
5467
+ });
5468
+ this.logger.warn('Background scan: input box text (not ours)', { sessionName, inputPreview: reading.text.slice(0, 80) });
5469
+ try {
5470
+ const result = await this.sendMessageToAgent(sessionName, entry.message);
5471
+ if (result.success) {
5472
+ entry.recovered = true;
5473
+ }
5474
+ else {
5475
+ entry.recoveryAttempts++;
5476
+ this.logger.error('Background scan: re-delivery failed — will try again next scan', { sessionName, error: result.error });
5477
+ }
5478
+ }
5479
+ catch (err) {
5480
+ entry.recoveryAttempts++;
5481
+ this.logger.error('Background scan: re-delivery threw — will try again next scan', {
5482
+ sessionName,
5483
+ error: err instanceof Error ? err.message : String(err),
5484
+ });
5485
+ }
5486
+ break;
5487
+ }
5488
+ // Our text is not in the box (someone else's is): ours was submitted.
5489
+ entry.recovered = true;
5490
+ continue;
5491
+ }
5440
5492
  entry.recoveryAttempts++;
5441
- this.logger.warn('Background scan: tracked message stuck, pressing Tab+Enter', {
5493
+ entry.unreadable = reading.state === 'unknown';
5494
+ if (reading.state === 'unknown') {
5495
+ // Cannot see the box: never press Enter blindly, never mark
5496
+ // it recovered — look again next scan; exhausting attempts
5497
+ // is logged as an error above.
5498
+ this.logger.warn('Background scan: tracked message near the bottom but the input box cannot be read — not pressing Enter', {
5499
+ sessionName,
5500
+ snippet: entry.snippet.slice(0, 50),
5501
+ attempt: entry.recoveryAttempts,
5502
+ });
5503
+ break;
5504
+ }
5505
+ this.logger.warn('Background scan: tracked message stuck in the input box, pressed Enter', {
5442
5506
  sessionName,
5443
5507
  snippet: entry.snippet.slice(0, 50),
5444
5508
  ageMs: now - entry.sentAt,
5445
5509
  attempt: entry.recoveryAttempts,
5446
5510
  maxAttempts: MAX_RECOVERY_ATTEMPTS,
5447
5511
  });
5448
- // Tab restores Ink TUI focus before Enter
5449
- await sessionHelper.sendKey(sessionName, 'Tab');
5450
- await delay(200);
5451
- await sessionHelper.sendEnter(sessionName);
5452
- await delay(500);
5453
- await sessionHelper.sendEnter(sessionName); // backup
5454
5512
  break; // One recovery per session per scan cycle
5455
5513
  }
5456
5514
  }
@@ -5501,7 +5559,7 @@ Loop until done, blocked, or explicitly reassigned:
5501
5559
  if (snippet.length < 10)
5502
5560
  return; // Too short to reliably match
5503
5561
  const entries = this.sentMessageTracker.get(sessionName) || [];
5504
- entries.push({ snippet, sentAt: Date.now(), recovered: false, recoveryAttempts: 0 });
5562
+ entries.push({ snippet, message, sentAt: Date.now(), recovered: false, recoveryAttempts: 0 });
5505
5563
  // Keep only last 5 minutes of entries
5506
5564
  const cutoff = Date.now() - 5 * 60 * 1000;
5507
5565
  this.sentMessageTracker.set(sessionName, entries.filter(e => e.sentAt > cutoff));
@@ -5823,33 +5881,9 @@ Loop until done, blocked, or explicitly reassigned:
5823
5881
  this.logger.info('Prompt delivery aborted before send (runtime exited)', { sessionName });
5824
5882
  return false;
5825
5883
  }
5826
- // Clear any pending input before sending the instruction (first attempt only).
5827
- if (attempt === 1) {
5828
- if (isClaudeCode) {
5829
- // Claude Code: Escape closes slash menus + Ctrl+U clears line.
5830
- await sessionHelper.sendEscape(sessionName);
5831
- await delay(200);
5832
- await sessionHelper.sendKey(sessionName, 'C-u');
5833
- await delay(300);
5834
- }
5835
- else if (runtimeType === RUNTIME_TYPES.CODEX_CLI || runtimeType === RUNTIME_TYPES.ANTIGRAVITY_CLI) {
5836
- // #246: Codex CLI — Ctrl+U to clear any stale input text.
5837
- // Unlike Gemini, Codex doesn't need Enter flush; its Ink TUI
5838
- // handles Ctrl+U for line clear similar to Claude Code.
5839
- // Antigravity clears its prompt box on Ctrl+U the same way.
5840
- await sessionHelper.sendKey(sessionName, 'C-u');
5841
- await delay(300);
5842
- }
5843
- else {
5844
- // Gemini CLI (Ink TUI): After /directory add processing, the TUI
5845
- // input may lose focus or have stale invisible characters.
5846
- // Send Enter to flush any residual input, then wait for the
5847
- // prompt to settle before sending the real instruction.
5848
- this.logger.debug('Gemini CLI pre-send: flushing input with Enter', { sessionName });
5849
- await sessionHelper.sendEnter(sessionName);
5850
- await delay(1000);
5851
- }
5852
- }
5884
+ // No pre-clearing (2026-10-03 review): Escape / Ctrl+U would wipe
5885
+ // whatever the box holds without knowing whose it is. The guarded
5886
+ // sendMessage below types only into a box it reads as empty.
5853
5887
  // Check abort right before writing instruction to terminal
5854
5888
  if (this.isFlowCancelled(sessionName, abortSignal)) {
5855
5889
  this.logger.info('Prompt delivery aborted before instruction send (runtime exited)', { sessionName });