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.
- package/config/roles/orchestrator/prompt.md +8 -9
- package/config/roles/team-leader/prompt.md +6 -0
- package/config/skills/agent/computer-use/SKILL.md +16 -0
- package/config/skills/agent/core/ask-owner/SKILL.md +14 -0
- package/config/skills/agent/core/ask-owner/execute.sh +19 -1
- package/config/skills/agent/core/ask-owner/execute.test.sh +12 -0
- package/config/skills/agent/core/gmail-send/SKILL.md +16 -0
- package/config/skills/agent/core/reply-channel/SKILL.md +16 -0
- package/config/skills/agent/core/slack-post/SKILL.md +16 -0
- package/config/skills/agent/core/whatsapp-send/SKILL.md +16 -0
- package/config/skills/agent/remote-browser/SKILL.md +43 -7
- package/config/skills/registry.json +7 -7
- package/dist/backend/backend/src/constants.d.ts +143 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +159 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +12 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +118 -3
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.js +5 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +37 -52
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +13 -0
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +17 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +102 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.js +5 -8
- package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +12 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +275 -241
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.js +5 -6
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts +6 -0
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.js +2 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js +12 -8
- package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +32 -27
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/owner-stopped.registry.d.ts +44 -0
- package/dist/backend/backend/src/services/agent/owner-stopped.registry.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/owner-stopped.registry.js +54 -0
- package/dist/backend/backend/src/services/agent/owner-stopped.registry.js.map +1 -0
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +4 -4
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-exit-monitor.service.js +4 -8
- package/dist/backend/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +19 -4
- package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.js +28 -5
- package/dist/backend/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts +2 -2
- package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-approval.service.js +13 -2
- package/dist/backend/backend/src/services/browser/browser-approval.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-outbound-guard.d.ts +214 -0
- package/dist/backend/backend/src/services/browser/browser-outbound-guard.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-outbound-guard.js +466 -0
- package/dist/backend/backend/src/services/browser/browser-outbound-guard.js.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts +23 -2
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-session.service.js +110 -62
- package/dist/backend/backend/src/services/browser/browser-session.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/held-action-store.d.ts +4 -0
- package/dist/backend/backend/src/services/browser/held-action-store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/held-action-store.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js +6 -8
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts +129 -0
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js +196 -0
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +42 -0
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +88 -10
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/agent-hook-state.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/agent-hook-state.js +5 -0
- package/dist/backend/backend/src/services/monitoring/agent-hook-state.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +5 -9
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +11 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +14 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.d.ts +37 -0
- package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.js +78 -0
- package/dist/backend/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-backend.interface.d.ts +13 -0
- package/dist/backend/backend/src/services/session/session-backend.interface.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-backend.interface.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts +118 -105
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js +267 -298
- package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/backend/backend/src/services/session/tui-input-guard.d.ts +144 -0
- package/dist/backend/backend/src/services/session/tui-input-guard.d.ts.map +1 -0
- package/dist/backend/backend/src/services/session/tui-input-guard.js +312 -0
- package/dist/backend/backend/src/services/session/tui-input-guard.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +14 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +31 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +82 -4
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/trace/trace.types.d.ts +2 -2
- package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/trace/trace.types.js +4 -0
- package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -1
- package/dist/backend/backend/src/services/trace/turn-origin.d.ts +70 -0
- package/dist/backend/backend/src/services/trace/turn-origin.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/turn-origin.js +198 -0
- package/dist/backend/backend/src/services/trace/turn-origin.js.map +1 -0
- package/dist/backend/backend/src/types/slack.types.d.ts +8 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/terminal-string-ops.js +3 -2
- package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +143 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +159 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +12 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +275 -241
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.js +5 -6
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts +6 -0
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.js +2 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js +12 -8
- package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.js +32 -27
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +4 -4
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js +4 -8
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +19 -4
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js +28 -5
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts +129 -0
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js +196 -0
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +42 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +88 -10
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts +11 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +14 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts +37 -0
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js +78 -0
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -1
- package/dist/cli/backend/src/services/session/session-backend.interface.d.ts +13 -0
- package/dist/cli/backend/src/services/session/session-backend.interface.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/session-backend.interface.js.map +1 -1
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts +118 -105
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/session-command-helper.js +267 -298
- package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/cli/backend/src/services/session/tui-input-guard.d.ts +144 -0
- package/dist/cli/backend/src/services/session/tui-input-guard.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/tui-input-guard.js +312 -0
- package/dist/cli/backend/src/services/session/tui-input-guard.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +14 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +31 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +82 -4
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/trace/trace.types.d.ts +2 -2
- package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -1
- package/dist/cli/backend/src/services/trace/trace.types.js +4 -0
- package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -1
- package/dist/cli/backend/src/services/trace/turn-origin.d.ts +70 -0
- package/dist/cli/backend/src/services/trace/turn-origin.d.ts.map +1 -0
- package/dist/cli/backend/src/services/trace/turn-origin.js +198 -0
- package/dist/cli/backend/src/services/trace/turn-origin.js.map +1 -0
- package/dist/cli/backend/src/types/slack.types.d.ts +8 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -1
- package/dist/cli/backend/src/utils/terminal-string-ops.js +3 -2
- package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts +0 -118
- package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/agent/agent-suspend.service.js +0 -304
- package/dist/cli/backend/src/services/agent/agent-suspend.service.js.map +0 -1
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts +0 -100
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.js +0 -149
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.js.map +0 -1
|
@@ -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,
|
|
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
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
*
|