@hyperdrive.bot/fleet-server 0.3.99
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/.env.example +31 -0
- package/README.md +111 -0
- package/dist/scripts/dev-runner.js +37 -0
- package/dist/scripts/mcp-stdio-socket-bridge-cli.mjs +62 -0
- package/dist/scripts/supervisor-entrypoint.js +156 -0
- package/dist/scripts/supervisor-log-config.js +31 -0
- package/dist/scripts/supervisor.js +345 -0
- package/dist/server/executable-resolution/executable-resolution.d.ts +10 -0
- package/dist/server/executable-resolution/executable-resolution.js +118 -0
- package/dist/server/executable-resolution/windows.d.ts +18 -0
- package/dist/server/executable-resolution/windows.js +65 -0
- package/dist/server/extensions/daemon-backend.d.ts +11 -0
- package/dist/server/extensions/daemon-backend.js +146 -0
- package/dist/server/extensions/extension-host-manager.d.ts +48 -0
- package/dist/server/extensions/extension-host-manager.js +141 -0
- package/dist/server/extensions/extension-loader.d.ts +11 -0
- package/dist/server/extensions/extension-loader.js +47 -0
- package/dist/server/extensions/extension-service.d.ts +17 -0
- package/dist/server/extensions/extension-service.js +76 -0
- package/dist/server/server/agent/activity-curator.d.ts +28 -0
- package/dist/server/server/agent/activity-curator.js +227 -0
- package/dist/server/server/agent/agent-archive.d.ts +11 -0
- package/dist/server/server/agent/agent-archive.js +16 -0
- package/dist/server/server/agent/agent-list-filter.d.ts +76 -0
- package/dist/server/server/agent/agent-list-filter.js +184 -0
- package/dist/server/server/agent/agent-loading.d.ts +12 -0
- package/dist/server/server/agent/agent-loading.js +76 -0
- package/dist/server/server/agent/agent-manager.d.ts +628 -0
- package/dist/server/server/agent/agent-manager.js +3370 -0
- package/dist/server/server/agent/agent-projections.d.ts +65 -0
- package/dist/server/server/agent/agent-projections.js +548 -0
- package/dist/server/server/agent/agent-prompt.d.ts +65 -0
- package/dist/server/server/agent/agent-prompt.js +271 -0
- package/dist/server/server/agent/agent-response-loop.d.ts +67 -0
- package/dist/server/server/agent/agent-response-loop.js +326 -0
- package/dist/server/server/agent/agent-routes.d.ts +18 -0
- package/dist/server/server/agent/agent-routes.js +107 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +809 -0
- package/dist/server/server/agent/agent-sdk-types.js +22 -0
- package/dist/server/server/agent/agent-storage.d.ts +209 -0
- package/dist/server/server/agent/agent-storage.js +365 -0
- package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
- package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
- package/dist/server/server/agent/agent-timeline-store-types.d.ts +50 -0
- package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
- package/dist/server/server/agent/agent-timeline-store.d.ts +27 -0
- package/dist/server/server/agent/agent-timeline-store.js +277 -0
- package/dist/server/server/agent/audio-utils.d.ts +3 -0
- package/dist/server/server/agent/audio-utils.js +19 -0
- package/dist/server/server/agent/authorship-ledger.d.ts +139 -0
- package/dist/server/server/agent/authorship-ledger.js +207 -0
- package/dist/server/server/agent/authorship-markup.d.ts +198 -0
- package/dist/server/server/agent/authorship-markup.js +288 -0
- package/dist/server/server/agent/blocker-log.d.ts +81 -0
- package/dist/server/server/agent/blocker-log.js +113 -0
- package/dist/server/server/agent/card-move-log.d.ts +66 -0
- package/dist/server/server/agent/card-move-log.js +113 -0
- package/dist/server/server/agent/create-agent/create.d.ts +126 -0
- package/dist/server/server/agent/create-agent/create.js +378 -0
- package/dist/server/server/agent/create-agent/routine.d.ts +45 -0
- package/dist/server/server/agent/create-agent/routine.js +53 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +54 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +152 -0
- package/dist/server/server/agent/create-agent-mode.d.ts +13 -0
- package/dist/server/server/agent/create-agent-mode.js +62 -0
- package/dist/server/server/agent/create-agent-title.d.ts +10 -0
- package/dist/server/server/agent/create-agent-title.js +34 -0
- package/dist/server/server/agent/dictation-debug.d.ts +13 -0
- package/dist/server/server/agent/dictation-debug.js +50 -0
- package/dist/server/server/agent/foreground-run-state.d.ts +50 -0
- package/dist/server/server/agent/foreground-run-state.js +160 -0
- package/dist/server/server/agent/import-sessions.d.ts +52 -0
- package/dist/server/server/agent/import-sessions.js +170 -0
- package/dist/server/server/agent/jarvis-judge.d.ts +157 -0
- package/dist/server/server/agent/jarvis-judge.js +289 -0
- package/dist/server/server/agent/judge-relay-gate.d.ts +70 -0
- package/dist/server/server/agent/judge-relay-gate.js +92 -0
- package/dist/server/server/agent/lifecycle-command.d.ts +75 -0
- package/dist/server/server/agent/lifecycle-command.js +97 -0
- package/dist/server/server/agent/mcp-server.d.ts +22 -0
- package/dist/server/server/agent/mcp-server.js +653 -0
- package/dist/server/server/agent/mcp-shared.d.ts +239 -0
- package/dist/server/server/agent/mcp-shared.js +200 -0
- package/dist/server/server/agent/output-language.d.ts +16 -0
- package/dist/server/server/agent/output-language.js +62 -0
- package/dist/server/server/agent/pcm16-resampler.d.ts +14 -0
- package/dist/server/server/agent/pcm16-resampler.js +63 -0
- package/dist/server/server/agent/permission-response.d.ts +15 -0
- package/dist/server/server/agent/permission-response.js +14 -0
- package/dist/server/server/agent/prompt-attachments.d.ts +7 -0
- package/dist/server/server/agent/prompt-attachments.js +90 -0
- package/dist/server/server/agent/provider-config-reload.d.ts +27 -0
- package/dist/server/server/agent/provider-config-reload.js +65 -0
- package/dist/server/server/agent/provider-history-timestamps.d.ts +2 -0
- package/dist/server/server/agent/provider-history-timestamps.js +16 -0
- package/dist/server/server/agent/provider-launch-config.d.ts +51 -0
- package/dist/server/server/agent/provider-launch-config.js +183 -0
- package/dist/server/server/agent/provider-notices.d.ts +3 -0
- package/dist/server/server/agent/provider-notices.js +5 -0
- package/dist/server/server/agent/provider-registry-contract.d.ts +40 -0
- package/dist/server/server/agent/provider-registry-contract.js +28 -0
- package/dist/server/server/agent/provider-registry.d.ts +97 -0
- package/dist/server/server/agent/provider-registry.js +686 -0
- package/dist/server/server/agent/provider-session-import.d.ts +10 -0
- package/dist/server/server/agent/provider-session-import.js +49 -0
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +166 -0
- package/dist/server/server/agent/provider-snapshot-manager.js +760 -0
- package/dist/server/server/agent/providers/acp-agent.d.ts +354 -0
- package/dist/server/server/agent/providers/acp-agent.js +2438 -0
- package/dist/server/server/agent/providers/claude/agent.d.ts +491 -0
- package/dist/server/server/agent/providers/claude/agent.js +5128 -0
- package/dist/server/server/agent/providers/claude/background-shell-kill-keys.d.ts +32 -0
- package/dist/server/server/agent/providers/claude/background-shell-kill-keys.js +34 -0
- package/dist/server/server/agent/providers/claude/background-task-tracker.d.ts +217 -0
- package/dist/server/server/agent/providers/claude/background-task-tracker.js +511 -0
- package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +124 -0
- package/dist/server/server/agent/providers/claude/background-work-kinds.js +96 -0
- package/dist/server/server/agent/providers/claude/feature-definitions.d.ts +8 -0
- package/dist/server/server/agent/providers/claude/feature-definitions.js +24 -0
- package/dist/server/server/agent/providers/claude/hook-handler.d.ts +37 -0
- package/dist/server/server/agent/providers/claude/hook-handler.js +193 -0
- package/dist/server/server/agent/providers/claude/hooks/event-translator.d.ts +49 -0
- package/dist/server/server/agent/providers/claude/hooks/event-translator.js +151 -0
- package/dist/server/server/agent/providers/claude/image-compression.d.ts +10 -0
- package/dist/server/server/agent/providers/claude/image-compression.js +32 -0
- package/dist/server/server/agent/providers/claude/model-manifest.d.ts +116 -0
- package/dist/server/server/agent/providers/claude/model-manifest.js +279 -0
- package/dist/server/server/agent/providers/claude/models.d.ts +11 -0
- package/dist/server/server/agent/providers/claude/models.js +97 -0
- package/dist/server/server/agent/providers/claude/partial-json.d.ts +5 -0
- package/dist/server/server/agent/providers/claude/partial-json.js +306 -0
- package/dist/server/server/agent/providers/claude/project-dir.d.ts +6 -0
- package/dist/server/server/agent/providers/claude/project-dir.js +57 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +62 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.js +175 -0
- package/dist/server/server/agent/providers/claude/resume-source.d.ts +11 -0
- package/dist/server/server/agent/providers/claude/resume-source.js +37 -0
- package/dist/server/server/agent/providers/claude/rewind.d.ts +30 -0
- package/dist/server/server/agent/providers/claude/rewind.js +30 -0
- package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts +22 -0
- package/dist/server/server/agent/providers/claude/sidechain-tracker.js +232 -0
- package/dist/server/server/agent/providers/claude/spawn-config-writer.d.ts +70 -0
- package/dist/server/server/agent/providers/claude/spawn-config-writer.js +229 -0
- package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +47 -0
- package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +292 -0
- package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.d.ts +24 -0
- package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.js +23 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.d.ts +41 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.js +95 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.d.ts +68 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.js +133 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts +3 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +130 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +16 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +129 -0
- package/dist/server/server/agent/providers/claude/transport/claude-paths.d.ts +31 -0
- package/dist/server/server/agent/providers/claude/transport/claude-paths.js +63 -0
- package/dist/server/server/agent/providers/claude/transport/continue-flag.d.ts +31 -0
- package/dist/server/server/agent/providers/claude/transport/continue-flag.js +42 -0
- package/dist/server/server/agent/providers/claude/transport/pty-query.d.ts +493 -0
- package/dist/server/server/agent/providers/claude/transport/pty-query.js +2075 -0
- package/dist/server/server/agent/providers/claude/transport/pty.d.ts +153 -0
- package/dist/server/server/agent/providers/claude/transport/pty.js +367 -0
- package/dist/server/server/agent/providers/claude/transport/redact.d.ts +8 -0
- package/dist/server/server/agent/providers/claude/transport/redact.js +33 -0
- package/dist/server/server/agent/providers/claude/transport/sdk.d.ts +65 -0
- package/dist/server/server/agent/providers/claude/transport/sdk.js +219 -0
- package/dist/server/server/agent/providers/claude/transport/tmux.d.ts +141 -0
- package/dist/server/server/agent/providers/claude/transport/tmux.js +218 -0
- package/dist/server/server/agent/providers/claude/transport/transcript-sdk-reader.d.ts +53 -0
- package/dist/server/server/agent/providers/claude/transport/transcript-sdk-reader.js +243 -0
- package/dist/server/server/agent/providers/claude/transport/transcript-tailer.d.ts +28 -0
- package/dist/server/server/agent/providers/claude/transport/transcript-tailer.js +166 -0
- package/dist/server/server/agent/providers/claude/transport/truncate-session.d.ts +50 -0
- package/dist/server/server/agent/providers/claude/transport/truncate-session.js +121 -0
- package/dist/server/server/agent/providers/claude/transport/turn-id-reconciler.d.ts +36 -0
- package/dist/server/server/agent/providers/claude/transport/turn-id-reconciler.js +75 -0
- package/dist/server/server/agent/providers/claude/transport/types.d.ts +61 -0
- package/dist/server/server/agent/providers/claude/transport/types.js +2 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +90 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.js +262 -0
- package/dist/server/server/agent/providers/codex/rewind.d.ts +21 -0
- package/dist/server/server/agent/providers/codex/rewind.js +46 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.d.ts +36 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.js +217 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +12 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +104 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +24 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js +830 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +308 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.js +4515 -0
- package/dist/server/server/agent/providers/codex-feature-definitions.d.ts +11 -0
- package/dist/server/server/agent/providers/codex-feature-definitions.js +45 -0
- package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +26 -0
- package/dist/server/server/agent/providers/copilot-acp-agent.js +187 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +15 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.js +18 -0
- package/dist/server/server/agent/providers/diagnostic-utils.d.ts +36 -0
- package/dist/server/server/agent/providers/diagnostic-utils.js +355 -0
- package/dist/server/server/agent/providers/generic-acp-agent.d.ts +42 -0
- package/dist/server/server/agent/providers/generic-acp-agent.js +161 -0
- package/dist/server/server/agent/providers/kiro-acp-agent.d.ts +17 -0
- package/dist/server/server/agent/providers/kiro-acp-agent.js +78 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +85 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +1010 -0
- package/dist/server/server/agent/providers/mock-slow-provider.d.ts +14 -0
- package/dist/server/server/agent/providers/mock-slow-provider.js +39 -0
- package/dist/server/server/agent/providers/opencode/paths.d.ts +2 -0
- package/dist/server/server/agent/providers/opencode/paths.js +7 -0
- package/dist/server/server/agent/providers/opencode/rewind.d.ts +18 -0
- package/dist/server/server/agent/providers/opencode/rewind.js +14 -0
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts +91 -0
- package/dist/server/server/agent/providers/opencode/server-manager.js +425 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +25 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.js +39 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.d.ts +92 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.js +268 -0
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts +3 -0
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +256 -0
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +13 -0
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +53 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +293 -0
- package/dist/server/server/agent/providers/opencode-agent.js +2908 -0
- package/dist/server/server/agent/providers/pi/agent.d.ts +137 -0
- package/dist/server/server/agent/providers/pi/agent.js +1651 -0
- package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +21 -0
- package/dist/server/server/agent/providers/pi/cli-runtime.js +233 -0
- package/dist/server/server/agent/providers/pi/history-mapper.d.ts +9 -0
- package/dist/server/server/agent/providers/pi/history-mapper.js +140 -0
- package/dist/server/server/agent/providers/pi/rewind.d.ts +8 -0
- package/dist/server/server/agent/providers/pi/rewind.js +8 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +208 -0
- package/dist/server/server/agent/providers/pi/rpc-types.js +2 -0
- package/dist/server/server/agent/providers/pi/runtime.d.ts +57 -0
- package/dist/server/server/agent/providers/pi/runtime.js +56 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +16 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.js +346 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +83 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +184 -0
- package/dist/server/server/agent/providers/pi/tool-call-mapper.d.ts +112 -0
- package/dist/server/server/agent/providers/pi/tool-call-mapper.js +284 -0
- package/dist/server/server/agent/providers/provider-image-output.d.ts +25 -0
- package/dist/server/server/agent/providers/provider-image-output.js +111 -0
- package/dist/server/server/agent/providers/provider-runner.d.ts +27 -0
- package/dist/server/server/agent/providers/provider-runner.js +81 -0
- package/dist/server/server/agent/providers/test-utils/session-stream-adapter.d.ts +3 -0
- package/dist/server/server/agent/providers/test-utils/session-stream-adapter.js +60 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +884 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +778 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +25 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js +208 -0
- package/dist/server/server/agent/recordings-debug.d.ts +3 -0
- package/dist/server/server/agent/recordings-debug.js +19 -0
- package/dist/server/server/agent/render-file-attachment.d.ts +29 -0
- package/dist/server/server/agent/render-file-attachment.js +41 -0
- package/dist/server/server/agent/resolve-resume-cwd.d.ts +14 -0
- package/dist/server/server/agent/resolve-resume-cwd.js +49 -0
- package/dist/server/server/agent/rewind/rewind.d.ts +10 -0
- package/dist/server/server/agent/rewind/rewind.js +29 -0
- package/dist/server/server/agent/rewind/test-rewind-session.d.ts +48 -0
- package/dist/server/server/agent/rewind/test-rewind-session.js +88 -0
- package/dist/server/server/agent/runtime-mcp-config.d.ts +14 -0
- package/dist/server/server/agent/runtime-mcp-config.js +53 -0
- package/dist/server/server/agent/session-digest-generator.d.ts +64 -0
- package/dist/server/server/agent/session-digest-generator.js +306 -0
- package/dist/server/server/agent/session-digest.d.ts +81 -0
- package/dist/server/server/agent/session-digest.js +226 -0
- package/dist/server/server/agent/structured-generation-providers.d.ts +29 -0
- package/dist/server/server/agent/structured-generation-providers.js +236 -0
- package/dist/server/server/agent/stt-debug.d.ts +10 -0
- package/dist/server/server/agent/stt-debug.js +33 -0
- package/dist/server/server/agent/stt-manager.d.ts +47 -0
- package/dist/server/server/agent/stt-manager.js +263 -0
- package/dist/server/server/agent/system-prompt.d.ts +2 -0
- package/dist/server/server/agent/system-prompt.js +8 -0
- package/dist/server/server/agent/timeline-append.d.ts +10 -0
- package/dist/server/server/agent/timeline-append.js +27 -0
- package/dist/server/server/agent/timeline-projection.d.ts +71 -0
- package/dist/server/server/agent/timeline-projection.js +373 -0
- package/dist/server/server/agent/tools/paseo-tools.d.ts +66 -0
- package/dist/server/server/agent/tools/paseo-tools.js +2616 -0
- package/dist/server/server/agent/tools/read-only-surface.d.ts +53 -0
- package/dist/server/server/agent/tools/read-only-surface.js +69 -0
- package/dist/server/server/agent/tools/types.d.ts +41 -0
- package/dist/server/server/agent/tools/types.js +2 -0
- package/dist/server/server/agent/tts-debug.d.ts +8 -0
- package/dist/server/server/agent/tts-debug.js +24 -0
- package/dist/server/server/agent/tts-manager.d.ts +41 -0
- package/dist/server/server/agent/tts-manager.js +375 -0
- package/dist/server/server/agent/wait-for-agent-tracker.d.ts +15 -0
- package/dist/server/server/agent/wait-for-agent-tracker.js +53 -0
- package/dist/server/server/agent-attention-policy.d.ts +29 -0
- package/dist/server/server/agent-attention-policy.js +36 -0
- package/dist/server/server/atomic-file.d.ts +3 -0
- package/dist/server/server/atomic-file.js +19 -0
- package/dist/server/server/auth.d.ts +68 -0
- package/dist/server/server/auth.js +207 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +40 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +92 -0
- package/dist/server/server/auto-archive-on-merge/index.d.ts +8 -0
- package/dist/server/server/auto-archive-on-merge/index.js +15 -0
- package/dist/server/server/bootstrap.d.ts +163 -0
- package/dist/server/server/bootstrap.js +1597 -0
- package/dist/server/server/browser-tools/broker.d.ts +45 -0
- package/dist/server/server/browser-tools/broker.js +390 -0
- package/dist/server/server/browser-tools/errors.d.ts +22 -0
- package/dist/server/server/browser-tools/errors.js +27 -0
- package/dist/server/server/browser-tools/policy.d.ts +10 -0
- package/dist/server/server/browser-tools/policy.js +16 -0
- package/dist/server/server/browser-tools/tools.d.ts +16 -0
- package/dist/server/server/browser-tools/tools.js +845 -0
- package/dist/server/server/chat/chat-mentions.d.ts +47 -0
- package/dist/server/server/chat/chat-mentions.js +113 -0
- package/dist/server/server/chat/chat-service.d.ts +78 -0
- package/dist/server/server/chat/chat-service.js +337 -0
- package/dist/server/server/checkout/status-projection.d.ts +19 -0
- package/dist/server/server/checkout/status-projection.js +102 -0
- package/dist/server/server/checkout-diff-manager.d.ts +41 -0
- package/dist/server/server/checkout-diff-manager.js +189 -0
- package/dist/server/server/checkout-git-utils.d.ts +11 -0
- package/dist/server/server/checkout-git-utils.js +34 -0
- package/dist/server/server/client-message-id.d.ts +3 -0
- package/dist/server/server/client-message-id.js +12 -0
- package/dist/server/server/config.d.ts +20 -0
- package/dist/server/server/config.js +427 -0
- package/dist/server/server/connection-offer.d.ts +20 -0
- package/dist/server/server/connection-offer.js +59 -0
- package/dist/server/server/daemon-config-store.d.ts +78 -0
- package/dist/server/server/daemon-config-store.js +320 -0
- package/dist/server/server/daemon-keypair.d.ts +8 -0
- package/dist/server/server/daemon-keypair.js +42 -0
- package/dist/server/server/daemon-version.d.ts +5 -0
- package/dist/server/server/daemon-version.js +22 -0
- package/dist/server/server/daemon-worker.d.ts +2 -0
- package/dist/server/server/daemon-worker.js +251 -0
- package/dist/server/server/dictation/dictation-stream-manager.d.ts +95 -0
- package/dist/server/server/dictation/dictation-stream-manager.js +581 -0
- package/dist/server/server/editor-targets.d.ts +18 -0
- package/dist/server/server/editor-targets.js +109 -0
- package/dist/server/server/ensure-workspace-for-create.d.ts +30 -0
- package/dist/server/server/ensure-workspace-for-create.js +20 -0
- package/dist/server/server/exports.d.ts +28 -0
- package/dist/server/server/exports.js +31 -0
- package/dist/server/server/file-download/token-store.d.ts +24 -0
- package/dist/server/server/file-download/token-store.js +40 -0
- package/dist/server/server/file-explorer/file-download-frames.d.ts +27 -0
- package/dist/server/server/file-explorer/file-download-frames.js +47 -0
- package/dist/server/server/file-explorer/mime-types.d.ts +58 -0
- package/dist/server/server/file-explorer/mime-types.js +212 -0
- package/dist/server/server/file-explorer/service.d.ts +97 -0
- package/dist/server/server/file-explorer/service.js +362 -0
- package/dist/server/server/file-upload/index.d.ts +27 -0
- package/dist/server/server/file-upload/index.js +158 -0
- package/dist/server/server/fleet/decision-service.d.ts +47 -0
- package/dist/server/server/fleet/decision-service.js +156 -0
- package/dist/server/server/fleet/fleet-controls.d.ts +77 -0
- package/dist/server/server/fleet/fleet-controls.js +217 -0
- package/dist/server/server/fleet/fleet-reader.d.ts +106 -0
- package/dist/server/server/fleet/fleet-reader.js +470 -0
- package/dist/server/server/fleet/fleet-scaffold.d.ts +26 -0
- package/dist/server/server/fleet/fleet-scaffold.js +48 -0
- package/dist/server/server/fleet/fleet-scheduler.d.ts +75 -0
- package/dist/server/server/fleet/fleet-scheduler.js +186 -0
- package/dist/server/server/fleet/fleet-service.d.ts +75 -0
- package/dist/server/server/fleet/fleet-service.js +134 -0
- package/dist/server/server/hostnames.d.ts +13 -0
- package/dist/server/server/hostnames.js +94 -0
- package/dist/server/server/ingestion/adapters.d.ts +21 -0
- package/dist/server/server/ingestion/adapters.js +27 -0
- package/dist/server/server/ingestion/apply.d.ts +220 -0
- package/dist/server/server/ingestion/apply.js +345 -0
- package/dist/server/server/ingestion/backfill.d.ts +123 -0
- package/dist/server/server/ingestion/backfill.js +160 -0
- package/dist/server/server/ingestion/cap-limiter.d.ts +148 -0
- package/dist/server/server/ingestion/cap-limiter.js +264 -0
- package/dist/server/server/ingestion/cohort-recorder-memory.d.ts +25 -0
- package/dist/server/server/ingestion/cohort-recorder-memory.js +78 -0
- package/dist/server/server/ingestion/cohort-recorder-store.d.ts +12 -0
- package/dist/server/server/ingestion/cohort-recorder-store.js +99 -0
- package/dist/server/server/ingestion/cohort-resume.d.ts +22 -0
- package/dist/server/server/ingestion/cohort-resume.js +171 -0
- package/dist/server/server/ingestion/cohort-store.d.ts +227 -0
- package/dist/server/server/ingestion/cohort-store.js +462 -0
- package/dist/server/server/ingestion/composio/gateway.d.ts +8 -0
- package/dist/server/server/ingestion/composio/gateway.js +469 -0
- package/dist/server/server/ingestion/dry-run.d.ts +70 -0
- package/dist/server/server/ingestion/dry-run.js +142 -0
- package/dist/server/server/ingestion/errors.d.ts +48 -0
- package/dist/server/server/ingestion/errors.js +63 -0
- package/dist/server/server/ingestion/external-user-id.d.ts +24 -0
- package/dist/server/server/ingestion/external-user-id.js +35 -0
- package/dist/server/server/ingestion/filters/store.d.ts +28 -0
- package/dist/server/server/ingestion/filters/store.js +74 -0
- package/dist/server/server/ingestion/fingerprint.d.ts +13 -0
- package/dist/server/server/ingestion/fingerprint.js +31 -0
- package/dist/server/server/ingestion/grouping.d.ts +26 -0
- package/dist/server/server/ingestion/grouping.js +116 -0
- package/dist/server/server/ingestion/hard-failure.d.ts +73 -0
- package/dist/server/server/ingestion/hard-failure.js +125 -0
- package/dist/server/server/ingestion/health.d.ts +38 -0
- package/dist/server/server/ingestion/health.js +90 -0
- package/dist/server/server/ingestion/ledger-schema.d.ts +99 -0
- package/dist/server/server/ingestion/ledger-schema.js +359 -0
- package/dist/server/server/ingestion/ledger.d.ts +562 -0
- package/dist/server/server/ingestion/ledger.js +881 -0
- package/dist/server/server/ingestion/match.d.ts +57 -0
- package/dist/server/server/ingestion/match.js +102 -0
- package/dist/server/server/ingestion/pipedream/gateway.d.ts +8 -0
- package/dist/server/server/ingestion/pipedream/gateway.js +282 -0
- package/dist/server/server/ingestion/presets.d.ts +57 -0
- package/dist/server/server/ingestion/presets.js +57 -0
- package/dist/server/server/ingestion/reconcile/artifact-kind.d.ts +19 -0
- package/dist/server/server/ingestion/reconcile/artifact-kind.js +65 -0
- package/dist/server/server/ingestion/reconcile/calendar.d.ts +19 -0
- package/dist/server/server/ingestion/reconcile/calendar.js +110 -0
- package/dist/server/server/ingestion/reconcile/constants.d.ts +60 -0
- package/dist/server/server/ingestion/reconcile/constants.js +62 -0
- package/dist/server/server/ingestion/reconcile/github.d.ts +21 -0
- package/dist/server/server/ingestion/reconcile/github.js +83 -0
- package/dist/server/server/ingestion/reconcile/gitlab.d.ts +23 -0
- package/dist/server/server/ingestion/reconcile/gitlab.js +94 -0
- package/dist/server/server/ingestion/reconcile/gmail.d.ts +9 -0
- package/dist/server/server/ingestion/reconcile/gmail.js +141 -0
- package/dist/server/server/ingestion/reconcile/jira.d.ts +17 -0
- package/dist/server/server/ingestion/reconcile/jira.js +76 -0
- package/dist/server/server/ingestion/reconcile/kinds.d.ts +16 -0
- package/dist/server/server/ingestion/reconcile/kinds.js +59 -0
- package/dist/server/server/ingestion/reconcile/mcp-call.d.ts +18 -0
- package/dist/server/server/ingestion/reconcile/mcp-call.js +105 -0
- package/dist/server/server/ingestion/reconcile/payload.d.ts +38 -0
- package/dist/server/server/ingestion/reconcile/payload.js +82 -0
- package/dist/server/server/ingestion/reconcile/registry.d.ts +40 -0
- package/dist/server/server/ingestion/reconcile/registry.js +72 -0
- package/dist/server/server/ingestion/reconcile/runner.d.ts +43 -0
- package/dist/server/server/ingestion/reconcile/runner.js +91 -0
- package/dist/server/server/ingestion/reconcile/sentry.d.ts +20 -0
- package/dist/server/server/ingestion/reconcile/sentry.js +85 -0
- package/dist/server/server/ingestion/reconcile/service.d.ts +134 -0
- package/dist/server/server/ingestion/reconcile/service.js +409 -0
- package/dist/server/server/ingestion/reconcile/signature-guard.d.ts +30 -0
- package/dist/server/server/ingestion/reconcile/signature-guard.js +82 -0
- package/dist/server/server/ingestion/reconcile/testing/fake-mcp-server.d.ts +68 -0
- package/dist/server/server/ingestion/reconcile/testing/fake-mcp-server.js +158 -0
- package/dist/server/server/ingestion/reconcile/types.d.ts +65 -0
- package/dist/server/server/ingestion/reconcile/types.js +11 -0
- package/dist/server/server/ingestion/reconcile/verdict-mapping.d.ts +54 -0
- package/dist/server/server/ingestion/reconcile/verdict-mapping.js +82 -0
- package/dist/server/server/ingestion/resolve-items.d.ts +23 -0
- package/dist/server/server/ingestion/resolve-items.js +54 -0
- package/dist/server/server/ingestion/run-projection.d.ts +35 -0
- package/dist/server/server/ingestion/run-projection.js +76 -0
- package/dist/server/server/ingestion/service.d.ts +147 -0
- package/dist/server/server/ingestion/service.js +364 -0
- package/dist/server/server/ingestion/sources/store.d.ts +27 -0
- package/dist/server/server/ingestion/sources/store.js +71 -0
- package/dist/server/server/ingestion/testing/claim-and-hang.d.ts +2 -0
- package/dist/server/server/ingestion/testing/claim-and-hang.js +21 -0
- package/dist/server/server/ingestion/testing/cohort-crash.d.ts +2 -0
- package/dist/server/server/ingestion/testing/cohort-crash.js +89 -0
- package/dist/server/server/ingestion/types.d.ts +226 -0
- package/dist/server/server/ingestion/types.js +18 -0
- package/dist/server/server/ingestion/utc-day.d.ts +27 -0
- package/dist/server/server/ingestion/utc-day.js +44 -0
- package/dist/server/server/json-utils.d.ts +11 -0
- package/dist/server/server/json-utils.js +45 -0
- package/dist/server/server/lifecycle-reasons.d.ts +4 -0
- package/dist/server/server/lifecycle-reasons.js +6 -0
- package/dist/server/server/logger.d.ts +29 -0
- package/dist/server/server/logger.js +125 -0
- package/dist/server/server/loop-service.d.ts +234 -0
- package/dist/server/server/loop-service.js +769 -0
- package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
- package/dist/server/server/managed-processes/managed-processes.js +326 -0
- package/dist/server/server/meetings/capture-http.d.ts +3 -0
- package/dist/server/server/meetings/capture-http.js +71 -0
- package/dist/server/server/meetings/capture-orchestrator.d.ts +123 -0
- package/dist/server/server/meetings/capture-orchestrator.js +531 -0
- package/dist/server/server/meetings/capture-stream-ws.d.ts +42 -0
- package/dist/server/server/meetings/capture-stream-ws.js +68 -0
- package/dist/server/server/meetings/live-transcription.d.ts +57 -0
- package/dist/server/server/meetings/live-transcription.js +280 -0
- package/dist/server/server/meetings/meetings-http.d.ts +42 -0
- package/dist/server/server/meetings/meetings-http.js +266 -0
- package/dist/server/server/messages.d.ts +9 -0
- package/dist/server/server/messages.js +29 -0
- package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
- package/dist/server/server/migrations/backfill-workspace-id.migration.js +64 -0
- package/dist/server/server/monitoring/telemetry.d.ts +13 -0
- package/dist/server/server/monitoring/telemetry.js +21 -0
- package/dist/server/server/package-version.d.ts +19 -0
- package/dist/server/server/package-version.js +60 -0
- package/dist/server/server/pagination/cursor.d.ts +16 -0
- package/dist/server/server/pagination/cursor.js +62 -0
- package/dist/server/server/pagination/sortable-pager.d.ts +24 -0
- package/dist/server/server/pagination/sortable-pager.js +68 -0
- package/dist/server/server/pairing-offer.d.ts +18 -0
- package/dist/server/server/pairing-offer.js +47 -0
- package/dist/server/server/pairing-qr.d.ts +7 -0
- package/dist/server/server/pairing-qr.js +45 -0
- package/dist/server/server/paseo-env.d.ts +13 -0
- package/dist/server/server/paseo-env.js +53 -0
- package/dist/server/server/paseo-home.d.ts +2 -0
- package/dist/server/server/paseo-home.js +19 -0
- package/dist/server/server/paseo-worktree-service.d.ts +52 -0
- package/dist/server/server/paseo-worktree-service.js +294 -0
- package/dist/server/server/path-utils.d.ts +5 -0
- package/dist/server/server/path-utils.js +34 -0
- package/dist/server/server/persisted-config.d.ts +390 -0
- package/dist/server/server/persisted-config.js +424 -0
- package/dist/server/server/persistence-hooks.d.ts +32 -0
- package/dist/server/server/persistence-hooks.js +99 -0
- package/dist/server/server/pid-lock.d.ts +32 -0
- package/dist/server/server/pid-lock.js +170 -0
- package/dist/server/server/private-files.d.ts +6 -0
- package/dist/server/server/private-files.js +37 -0
- package/dist/server/server/process-diagnostics.d.ts +17 -0
- package/dist/server/server/process-diagnostics.js +22 -0
- package/dist/server/server/push/categories.d.ts +35 -0
- package/dist/server/server/push/categories.js +40 -0
- package/dist/server/server/push/expo-worker.d.ts +51 -0
- package/dist/server/server/push/expo-worker.js +188 -0
- package/dist/server/server/push/index.d.ts +39 -0
- package/dist/server/server/push/index.js +45 -0
- package/dist/server/server/push/notifications.d.ts +9 -0
- package/dist/server/server/push/notifications.js +15 -0
- package/dist/server/server/push/push-service.d.ts +20 -0
- package/dist/server/server/push/push-service.js +66 -0
- package/dist/server/server/push/routes.d.ts +28 -0
- package/dist/server/server/push/routes.js +74 -0
- package/dist/server/server/push/subscriptions.d.ts +42 -0
- package/dist/server/server/push/subscriptions.js +112 -0
- package/dist/server/server/push/token-store.d.ts +29 -0
- package/dist/server/server/push/token-store.js +78 -0
- package/dist/server/server/push/triggers.d.ts +68 -0
- package/dist/server/server/push/triggers.js +103 -0
- package/dist/server/server/push/vapid-keys.d.ts +15 -0
- package/dist/server/server/push/vapid-keys.js +52 -0
- package/dist/server/server/push/web-worker.d.ts +44 -0
- package/dist/server/server/push/web-worker.js +99 -0
- package/dist/server/server/reference-enricher.d.ts +41 -0
- package/dist/server/server/reference-enricher.js +392 -0
- package/dist/server/server/relay-transport.d.ts +31 -0
- package/dist/server/server/relay-transport.js +545 -0
- package/dist/server/server/resolve-workspace-id-for-path.d.ts +4 -0
- package/dist/server/server/resolve-workspace-id-for-path.js +71 -0
- package/dist/server/server/resolve-worktree-creation-intent.d.ts +32 -0
- package/dist/server/server/resolve-worktree-creation-intent.js +121 -0
- package/dist/server/server/restart-manifest.d.ts +83 -0
- package/dist/server/server/restart-manifest.js +122 -0
- package/dist/server/server/resume-after-restart.d.ts +50 -0
- package/dist/server/server/resume-after-restart.js +108 -0
- package/dist/server/server/schedule/create-cadence.d.ts +22 -0
- package/dist/server/server/schedule/create-cadence.js +70 -0
- package/dist/server/server/schedule/cron.d.ts +8 -0
- package/dist/server/server/schedule/cron.js +114 -0
- package/dist/server/server/schedule/service.d.ts +207 -0
- package/dist/server/server/schedule/service.js +867 -0
- package/dist/server/server/schedule/source-prompt.d.ts +42 -0
- package/dist/server/server/schedule/source-prompt.js +87 -0
- package/dist/server/server/schedule/store.d.ts +13 -0
- package/dist/server/server/schedule/store.js +57 -0
- package/dist/server/server/script-health-monitor.d.ts +39 -0
- package/dist/server/server/script-health-monitor.js +159 -0
- package/dist/server/server/script-proxy.d.ts +3 -0
- package/dist/server/server/script-proxy.js +2 -0
- package/dist/server/server/script-route-branch-handler.d.ts +10 -0
- package/dist/server/server/script-route-branch-handler.js +11 -0
- package/dist/server/server/script-status-projection.d.ts +36 -0
- package/dist/server/server/script-status-projection.js +200 -0
- package/dist/server/server/search/extract.d.ts +53 -0
- package/dist/server/server/search/extract.js +222 -0
- package/dist/server/server/search/index-store.d.ts +83 -0
- package/dist/server/server/search/index-store.js +241 -0
- package/dist/server/server/search/indexer.d.ts +69 -0
- package/dist/server/server/search/indexer.js +362 -0
- package/dist/server/server/search/resolve-cwd.d.ts +40 -0
- package/dist/server/server/search/resolve-cwd.js +109 -0
- package/dist/server/server/search/session-search-service.d.ts +59 -0
- package/dist/server/server/search/session-search-service.js +171 -0
- package/dist/server/server/server-id.d.ts +17 -0
- package/dist/server/server/server-id.js +68 -0
- package/dist/server/server/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session/agent-config/agent-config-session.d.ts +50 -0
- package/dist/server/server/session/agent-config/agent-config-session.js +98 -0
- package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +59 -0
- package/dist/server/server/session/agent-updates/agent-updates-service.js +220 -0
- package/dist/server/server/session/chat/chat-ingestion-session.d.ts +285 -0
- package/dist/server/server/session/chat/chat-ingestion-session.js +504 -0
- package/dist/server/server/session/chat/chat-schedule-loop-session.d.ts +175 -0
- package/dist/server/server/session/chat/chat-schedule-loop-session.js +720 -0
- package/dist/server/server/session/checkout/checkout-session.d.ts +140 -0
- package/dist/server/server/session/checkout/checkout-session.js +927 -0
- package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
- package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
- package/dist/server/server/session/daemon/daemon-self-update-session-controller.d.ts +32 -0
- package/dist/server/server/session/daemon/daemon-self-update-session-controller.js +88 -0
- package/dist/server/server/session/daemon/daemon-self-updater.d.ts +32 -0
- package/dist/server/server/session/daemon/daemon-self-updater.js +56 -0
- package/dist/server/server/session/daemon/daemon-session.d.ts +89 -0
- package/dist/server/server/session/daemon/daemon-session.js +179 -0
- package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
- package/dist/server/server/session/daemon/diagnostics.js +431 -0
- package/dist/server/server/session/daemon/install-origin.d.ts +7 -0
- package/dist/server/server/session/daemon/install-origin.js +64 -0
- package/dist/server/server/session/daemon/npm-global-cli.d.ts +29 -0
- package/dist/server/server/session/daemon/npm-global-cli.js +98 -0
- package/dist/server/server/session/files/workspace-files-session.d.ts +53 -0
- package/dist/server/server/session/files/workspace-files-session.js +245 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
- package/dist/server/server/session/ingestion/source-session.d.ts +78 -0
- package/dist/server/server/session/ingestion/source-session.js +191 -0
- package/dist/server/server/session/project-config/project-config-session.d.ts +34 -0
- package/dist/server/server/session/project-config/project-config-session.js +125 -0
- package/dist/server/server/session/provider/provider-catalog-session.d.ts +105 -0
- package/dist/server/server/session/provider/provider-catalog-session.js +364 -0
- package/dist/server/server/session/session-rename-route.d.ts +4 -0
- package/dist/server/server/session/session-rename-route.js +71 -0
- package/dist/server/server/session/session-rename-service.d.ts +25 -0
- package/dist/server/server/session/session-rename-service.js +76 -0
- package/dist/server/server/session/voice/voice-session.d.ts +166 -0
- package/dist/server/server/session/voice/voice-session.js +899 -0
- package/dist/server/server/session/voice/voice-turn-controller.d.ts +38 -0
- package/dist/server/server/session/voice/voice-turn-controller.js +393 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +46 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +214 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
- package/dist/server/server/session.d.ts +677 -0
- package/dist/server/server/session.js +6531 -0
- package/dist/server/server/speech/audio.d.ts +10 -0
- package/dist/server/server/speech/audio.js +101 -0
- package/dist/server/server/speech/provider-resolver.d.ts +3 -0
- package/dist/server/server/speech/provider-resolver.js +7 -0
- package/dist/server/server/speech/providers/local/config.d.ts +30 -0
- package/dist/server/server/speech/providers/local/config.js +134 -0
- package/dist/server/server/speech/providers/local/models.d.ts +11 -0
- package/dist/server/server/speech/providers/local/models.js +17 -0
- package/dist/server/server/speech/providers/local/runtime.d.ts +31 -0
- package/dist/server/server/speech/providers/local/runtime.js +135 -0
- package/dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx +0 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +56 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.js +74 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts +15 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +142 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts +49 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +73 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts +9 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +107 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.d.ts +28 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +137 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.d.ts +21 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +133 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.d.ts +18 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.d.ts +21 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js +101 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.d.ts +19 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.js +49 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.d.ts +38 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.js +176 -0
- package/dist/server/server/speech/providers/local/worker-bytes.d.ts +4 -0
- package/dist/server/server/speech/providers/local/worker-bytes.js +9 -0
- package/dist/server/server/speech/providers/local/worker-client.d.ts +93 -0
- package/dist/server/server/speech/providers/local/worker-client.js +611 -0
- package/dist/server/server/speech/providers/local/worker-process.d.ts +2 -0
- package/dist/server/server/speech/providers/local/worker-process.js +270 -0
- package/dist/server/server/speech/providers/local/worker-protocol.d.ts +95 -0
- package/dist/server/server/speech/providers/local/worker-protocol.js +2 -0
- package/dist/server/server/speech/providers/openai/config.d.ts +19 -0
- package/dist/server/server/speech/providers/openai/config.js +148 -0
- package/dist/server/server/speech/providers/openai/runtime.d.ts +29 -0
- package/dist/server/server/speech/providers/openai/runtime.js +102 -0
- package/dist/server/server/speech/providers/openai/stt.d.ts +23 -0
- package/dist/server/server/speech/providers/openai/stt.js +209 -0
- package/dist/server/server/speech/providers/openai/tts.d.ts +19 -0
- package/dist/server/server/speech/providers/openai/tts.js +48 -0
- package/dist/server/server/speech/speech-config-resolver.d.ts +11 -0
- package/dist/server/server/speech/speech-config-resolver.js +132 -0
- package/dist/server/server/speech/speech-provider.d.ts +59 -0
- package/dist/server/server/speech/speech-provider.js +2 -0
- package/dist/server/server/speech/speech-runtime.d.ts +45 -0
- package/dist/server/server/speech/speech-runtime.js +562 -0
- package/dist/server/server/speech/speech-types.d.ts +22 -0
- package/dist/server/server/speech/speech-types.js +8 -0
- package/dist/server/server/speech/turn-detection-provider.d.ts +23 -0
- package/dist/server/server/speech/turn-detection-provider.js +2 -0
- package/dist/server/server/tenant-token-verifier.d.ts +43 -0
- package/dist/server/server/tenant-token-verifier.js +79 -0
- package/dist/server/server/update/installer.d.ts +70 -0
- package/dist/server/server/update/installer.js +181 -0
- package/dist/server/server/update/registry.d.ts +47 -0
- package/dist/server/server/update/registry.js +95 -0
- package/dist/server/server/update/slots.d.ts +45 -0
- package/dist/server/server/update/slots.js +97 -0
- package/dist/server/server/update/state.d.ts +41 -0
- package/dist/server/server/update/state.js +65 -0
- package/dist/server/server/uploads/file-attachment-validation.d.ts +61 -0
- package/dist/server/server/uploads/file-attachment-validation.js +56 -0
- package/dist/server/server/uploads/staging-service.d.ts +91 -0
- package/dist/server/server/uploads/staging-service.js +144 -0
- package/dist/server/server/utils/diff-highlighter.d.ts +57 -0
- package/dist/server/server/utils/diff-highlighter.js +279 -0
- package/dist/server/server/voice/voice-turn-controller.d.ts +38 -0
- package/dist/server/server/voice/voice-turn-controller.js +393 -0
- package/dist/server/server/voice-config.d.ts +15 -0
- package/dist/server/server/voice-config.js +54 -0
- package/dist/server/server/voice-permission-policy.d.ts +4 -0
- package/dist/server/server/voice-permission-policy.js +13 -0
- package/dist/server/server/voice-types.d.ts +12 -0
- package/dist/server/server/voice-types.js +2 -0
- package/dist/server/server/watch/composer-dictate.d.ts +42 -0
- package/dist/server/server/watch/composer-dictate.js +37 -0
- package/dist/server/server/watch/notification-action.d.ts +38 -0
- package/dist/server/server/watch/notification-action.js +54 -0
- package/dist/server/server/watch/notification-registry.d.ts +16 -0
- package/dist/server/server/watch/notification-registry.js +2 -0
- package/dist/server/server/web-ui.d.ts +10 -0
- package/dist/server/server/web-ui.js +247 -0
- package/dist/server/server/websocket/runtime-metrics.d.ts +94 -0
- package/dist/server/server/websocket/runtime-metrics.js +148 -0
- package/dist/server/server/websocket-server.d.ts +231 -0
- package/dist/server/server/websocket-server.js +1853 -0
- package/dist/server/server/whatsapp-bridge.d.ts +74 -0
- package/dist/server/server/whatsapp-bridge.js +142 -0
- package/dist/server/server/workflow/workflow-agent-resolution.d.ts +82 -0
- package/dist/server/server/workflow/workflow-agent-resolution.js +105 -0
- package/dist/server/server/workflow/workflow-feature.d.ts +2 -0
- package/dist/server/server/workflow/workflow-feature.js +8 -0
- package/dist/server/server/workflow/workflow-http-routes.d.ts +27 -0
- package/dist/server/server/workflow/workflow-http-routes.js +97 -0
- package/dist/server/server/workflow/workflow-manager.d.ts +363 -0
- package/dist/server/server/workflow/workflow-manager.js +852 -0
- package/dist/server/server/workflow/workflow-progress.d.ts +148 -0
- package/dist/server/server/workflow/workflow-progress.js +274 -0
- package/dist/server/server/workflow/workflow-storage.d.ts +36 -0
- package/dist/server/server/workflow/workflow-storage.js +143 -0
- package/dist/server/server/workspace/all-sessions-route.d.ts +32 -0
- package/dist/server/server/workspace/all-sessions-route.js +218 -0
- package/dist/server/server/workspace/recall-search-feature.d.ts +2 -0
- package/dist/server/server/workspace/recall-search-feature.js +31 -0
- package/dist/server/server/workspace/session-openable.d.ts +16 -0
- package/dist/server/server/workspace/session-openable.js +48 -0
- package/dist/server/server/workspace/sessions-route.d.ts +15 -0
- package/dist/server/server/workspace/sessions-route.js +108 -0
- package/dist/server/server/workspace/sessions-search-route.d.ts +40 -0
- package/dist/server/server/workspace/sessions-search-route.js +74 -0
- package/dist/server/server/workspace-archive-service.d.ts +65 -0
- package/dist/server/server/workspace-archive-service.js +229 -0
- package/dist/server/server/workspace-auto-name.d.ts +55 -0
- package/dist/server/server/workspace-auto-name.js +128 -0
- package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
- package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
- package/dist/server/server/workspace-directory.d.ts +93 -0
- package/dist/server/server/workspace-directory.js +440 -0
- package/dist/server/server/workspace-git-metadata.d.ts +24 -0
- package/dist/server/server/workspace-git-metadata.js +55 -0
- package/dist/server/server/workspace-git-service.d.ts +257 -0
- package/dist/server/server/workspace-git-service.js +1386 -0
- package/dist/server/server/workspace-reconciliation-service.d.ts +57 -0
- package/dist/server/server/workspace-reconciliation-service.js +278 -0
- package/dist/server/server/workspace-registry-bootstrap.d.ts +13 -0
- package/dist/server/server/workspace-registry-bootstrap.js +121 -0
- package/dist/server/server/workspace-registry-model.d.ts +55 -0
- package/dist/server/server/workspace-registry-model.js +190 -0
- package/dist/server/server/workspace-registry.d.ts +117 -0
- package/dist/server/server/workspace-registry.js +194 -0
- package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
- package/dist/server/server/workspace-script-runtime-store.js +78 -0
- package/dist/server/server/workspace-service-env.d.ts +18 -0
- package/dist/server/server/workspace-service-env.js +85 -0
- package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
- package/dist/server/server/workspace-service-port-registry.js +59 -0
- package/dist/server/server/worktree/commands.d.ts +55 -0
- package/dist/server/server/worktree/commands.js +107 -0
- package/dist/server/server/worktree-bootstrap.d.ts +76 -0
- package/dist/server/server/worktree-bootstrap.js +720 -0
- package/dist/server/server/worktree-branch-name-generator.d.ts +35 -0
- package/dist/server/server/worktree-branch-name-generator.js +91 -0
- package/dist/server/server/worktree-core.d.ts +31 -0
- package/dist/server/server/worktree-core.js +105 -0
- package/dist/server/server/worktree-errors.d.ts +12 -0
- package/dist/server/server/worktree-errors.js +28 -0
- package/dist/server/server/worktree-session.d.ts +152 -0
- package/dist/server/server/worktree-session.js +466 -0
- package/dist/server/services/github-service.d.ts +337 -0
- package/dist/server/services/github-service.js +2101 -0
- package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
- package/dist/server/services/quota-fetcher/manifest.js +52 -0
- package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
- package/dist/server/services/quota-fetcher/provider.js +2 -0
- package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
- package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
- package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
- package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
- package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
- package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
- package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
- package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
- package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
- package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
- package/dist/server/services/quota-fetcher/providers/minimax.d.ts +29 -0
- package/dist/server/services/quota-fetcher/providers/minimax.js +227 -0
- package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
- package/dist/server/services/quota-fetcher/service.d.ts +28 -0
- package/dist/server/services/quota-fetcher/service.js +58 -0
- package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
- package/dist/server/services/quota-fetcher/usage.js +49 -0
- package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
- package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
- package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
- package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
- package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
- package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
- package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
- package/dist/server/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
- package/dist/server/terminal/terminal-capture.d.ts +12 -0
- package/dist/server/terminal/terminal-capture.js +43 -0
- package/dist/server/terminal/terminal-manager-factory.d.ts +6 -0
- package/dist/server/terminal/terminal-manager-factory.js +5 -0
- package/dist/server/terminal/terminal-manager.d.ts +78 -0
- package/dist/server/terminal/terminal-manager.js +319 -0
- package/dist/server/terminal/terminal-output-coalescer.d.ts +34 -0
- package/dist/server/terminal/terminal-output-coalescer.js +73 -0
- package/dist/server/terminal/terminal-restore.d.ts +18 -0
- package/dist/server/terminal/terminal-restore.js +52 -0
- package/dist/server/terminal/terminal-session-controller.d.ts +85 -0
- package/dist/server/terminal/terminal-session-controller.js +819 -0
- package/dist/server/terminal/terminal-ts-loader.mjs +20 -0
- package/dist/server/terminal/terminal-worker-process.d.ts +2 -0
- package/dist/server/terminal/terminal-worker-process.js +304 -0
- package/dist/server/terminal/terminal-worker-protocol.d.ts +119 -0
- package/dist/server/terminal/terminal-worker-protocol.js +2 -0
- package/dist/server/terminal/terminal.d.ts +151 -0
- package/dist/server/terminal/terminal.js +1161 -0
- package/dist/server/terminal/worker-terminal-manager.d.ts +20 -0
- package/dist/server/terminal/worker-terminal-manager.js +688 -0
- package/dist/server/utils/build-metadata-prompt.d.ts +19 -0
- package/dist/server/utils/build-metadata-prompt.js +29 -0
- package/dist/server/utils/checkout-git.d.ts +253 -0
- package/dist/server/utils/checkout-git.js +2238 -0
- package/dist/server/utils/directory-suggestions.d.ts +26 -0
- package/dist/server/utils/directory-suggestions.js +860 -0
- package/dist/server/utils/git-rev-parse-path.d.ts +3 -0
- package/dist/server/utils/git-rev-parse-path.js +21 -0
- package/dist/server/utils/github-remote.d.ts +9 -0
- package/dist/server/utils/github-remote.js +62 -0
- package/dist/server/utils/paseo-config-file.d.ts +30 -0
- package/dist/server/utils/paseo-config-file.js +90 -0
- package/dist/server/utils/path.d.ts +18 -0
- package/dist/server/utils/path.js +123 -0
- package/dist/server/utils/project-icon.d.ts +39 -0
- package/dist/server/utils/project-icon.js +364 -0
- package/dist/server/utils/promise-timeout.d.ts +9 -0
- package/dist/server/utils/promise-timeout.js +25 -0
- package/dist/server/utils/run-git-command.d.ts +37 -0
- package/dist/server/utils/run-git-command.js +232 -0
- package/dist/server/utils/spawn.d.ts +33 -0
- package/dist/server/utils/spawn.js +71 -0
- package/dist/server/utils/string-command-shell.d.ts +10 -0
- package/dist/server/utils/string-command-shell.js +21 -0
- package/dist/server/utils/tree-kill.d.ts +18 -0
- package/dist/server/utils/tree-kill.js +77 -0
- package/dist/server/utils/windows-command.d.ts +15 -0
- package/dist/server/utils/windows-command.js +41 -0
- package/dist/server/utils/worktree-metadata.d.ts +39 -0
- package/dist/server/utils/worktree-metadata.js +174 -0
- package/dist/server/utils/worktree.d.ts +205 -0
- package/dist/server/utils/worktree.js +1119 -0
- package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css +1 -0
- package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.br +0 -0
- package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-67f19cbfce758da6aa23c38c2dbd6a33.js +5 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-67f19cbfce758da6aa23c38c2dbd6a33.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-67f19cbfce758da6aa23c38c2dbd6a33.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-67f19cbfce758da6aa23c38c2dbd6a33.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-67f19cbfce758da6aa23c38c2dbd6a33.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6888615a6b700f975c1f78501ad8577c.js +3 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6888615a6b700f975c1f78501ad8577c.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6888615a6b700f975c1f78501ad8577c.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6888615a6b700f975c1f78501ad8577c.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-6888615a6b700f975c1f78501ad8577c.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-dc1e81e7cbad84d30a01c48dcc9a5a3e.js +16683 -0
- package/dist/server/web-ui/_expo/static/js/web/index-dc1e81e7cbad84d30a01c48dcc9a5a3e.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-dc1e81e7cbad84d30a01c48dcc9a5a3e.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-dc1e81e7cbad84d30a01c48dcc9a5a3e.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-dc1e81e7cbad84d30a01c48dcc9a5a3e.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-991ae7fd00f9fed45084fbb39160e8b8.js +3 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-991ae7fd00f9fed45084fbb39160e8b8.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-991ae7fd00f9fed45084fbb39160e8b8.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-991ae7fd00f9fed45084fbb39160e8b8.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-991ae7fd00f9fed45084fbb39160e8b8.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-fdb4d787a259ce350f76280ca0e2bfa2.js +5 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-fdb4d787a259ce350f76280ca0e2bfa2.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-fdb4d787a259ce350f76280ca0e2bfa2.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-fdb4d787a259ce350f76280ca0e2bfa2.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-fdb4d787a259ce350f76280ca0e2bfa2.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-9502a8e721925a6353ec45f7613f537e.js +11 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-9502a8e721925a6353ec45f7613f537e.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-9502a8e721925a6353ec45f7613f537e.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-9502a8e721925a6353ec45f7613f537e.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-9502a8e721925a6353ec45f7613f537e.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-4ed5094aa059a0fc724f5ae70dd2f693.js +60 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-4ed5094aa059a0fc724f5ae70dd2f693.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-4ed5094aa059a0fc724f5ae70dd2f693.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-4ed5094aa059a0fc724f5ae70dd2f693.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-4ed5094aa059a0fc724f5ae70dd2f693.js.map.gz +0 -0
- package/dist/server/web-ui/apple-touch-icon.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/antigravity.6e91a685c33435e0b466a56db86cf141.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/cursor.c31d6bce4fe9aadc3fe59962f4c4fcf3.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/file-explorer.3e15e8f72c825c85ce336bcb0cdef776.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/finder.7f68fc2c475621a672e1be09309d5567.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/vscode.832bdb4c685d930f1c864c793703600b.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/webstorm.aa5dc2cd8c20cc0a155c4c5c5ab3c5f5.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/zed.f3a670b7f9aa226da4fe53fb86f1abbd.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-dark-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-dark-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-dark.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-light-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-light-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-light.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
- package/dist/server/web-ui/assets/assets/images/notification-icon.3bf81d33ddbf380606bdd248ba83e158.png +0 -0
- package/dist/server/web-ui/favicon.ico +0 -0
- package/dist/server/web-ui/index.html +196 -0
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/dist/server/web-ui/manifest.json +27 -0
- package/dist/server/web-ui/manifest.json.br +0 -0
- package/dist/server/web-ui/manifest.json.gz +0 -0
- package/dist/server/web-ui/metadata.json +1 -0
- package/dist/server/web-ui/metadata.json.br +1 -0
- package/dist/server/web-ui/metadata.json.gz +0 -0
- package/dist/server/web-ui/pwa-icon-192.png +0 -0
- package/dist/server/web-ui/pwa-icon-512.png +0 -0
- package/dist/server/web-ui/robots.txt +2 -0
- package/dist/server/web-ui/service-worker.js +87 -0
- package/dist/server/web-ui/service-worker.js.br +0 -0
- package/dist/server/web-ui/service-worker.js.gz +0 -0
- package/dist/src/executable-resolution/executable-resolution.js +118 -0
- package/dist/src/executable-resolution/windows.js +65 -0
- package/dist/src/server/agent/provider-launch-config.js +183 -0
- package/dist/src/server/paseo-env.js +53 -0
- package/dist/src/server/paseo-home.js +19 -0
- package/dist/src/server/persisted-config.js +424 -0
- package/dist/src/server/pid-lock.js +170 -0
- package/dist/src/server/private-files.js +37 -0
- package/dist/src/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
- package/dist/src/server/update/slots.js +97 -0
- package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
- package/dist/src/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
- package/dist/src/utils/spawn.js +71 -0
- package/dist/src/utils/windows-command.js +41 -0
- package/package.json +146 -0
|
@@ -0,0 +1,2075 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { PtyExitedError } from "./pty.js";
|
|
6
|
+
import { TranscriptSdkReader } from "./transcript-sdk-reader.js";
|
|
7
|
+
/**
|
|
8
|
+
* PtyQuery — a `Query`-shaped adapter over an interactive `claude` PTY.
|
|
9
|
+
*
|
|
10
|
+
* The query pump consumes `for await (message of query)` plus a handful of control
|
|
11
|
+
* methods. `SdkTransport` gets its `Query` straight from the SDK; the PTY transport has
|
|
12
|
+
* no SDK, so PtyQuery reconstructs the same `SDKMessage` stream:
|
|
13
|
+
* - user turns → typed into the PTY (write + Enter), gated on claude being ready
|
|
14
|
+
* - assistant / tool_result messages → tailed from the session transcript JSONL
|
|
15
|
+
* - turn end → detected from the transcript (an `assistant` with a terminal
|
|
16
|
+
* `stop_reason` after a short idle) → a synthesized `result` message,
|
|
17
|
+
* which is what makes the pump emit `turn_completed`.
|
|
18
|
+
*
|
|
19
|
+
* Turn-end is transcript-derived (not hook-derived) on purpose: it removes the
|
|
20
|
+
* `paseo agent-hook` → socket round-trip from the critical path. A `Stop`-hook fast-path
|
|
21
|
+
* can be layered on later.
|
|
22
|
+
*
|
|
23
|
+
* Control methods: `interrupt()` sends ESC to the TUI; `setModel`/`setPermissionMode`/
|
|
24
|
+
* thinking are already driven by the `instanceof PtyTransport` branches in agent.ts
|
|
25
|
+
* (which write to the transport directly), so here they are safe no-ops. The long tail
|
|
26
|
+
* of `Query` methods (rewindFiles, readFile, …) is unsupported on the PTY path; the
|
|
27
|
+
* instance is cast `as unknown as Query` at the seam.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Readiness is "the terminal stopped talking", not "the terminal said something".
|
|
31
|
+
*
|
|
32
|
+
* This used to schedule a settle 1500ms after EVERY chunk, and because settling is
|
|
33
|
+
* idempotent the first chunk always won: readiness fired 1.5s after the first byte no
|
|
34
|
+
* matter how much boot output was still streaming. The splash banner is a first byte, so
|
|
35
|
+
* on a slow start (seven MCP servers to connect) paseo began typing into a terminal that
|
|
36
|
+
* was not accepting input yet, and the prompt was lost outright. Observed 2026-08-01: a
|
|
37
|
+
* session whose entire recording was the splash line, then 70s of failed Enters.
|
|
38
|
+
*
|
|
39
|
+
* Resetting the timer on each chunk makes it a real debounce, so it settles once output
|
|
40
|
+
* has actually gone quiet. The ceiling is correspondingly generous, because waiting a few
|
|
41
|
+
* extra seconds costs nothing next to losing the message.
|
|
42
|
+
*/
|
|
43
|
+
const READY_QUIET_SETTLE_MS = 1500;
|
|
44
|
+
const READY_MAX_WAIT_MS = 60000;
|
|
45
|
+
/**
|
|
46
|
+
* How long the idle prompt must hold before we believe it. The TUI repaints its footer
|
|
47
|
+
* mid-turn, so a single frame without the busy marker is a flicker, not idleness.
|
|
48
|
+
*/
|
|
49
|
+
const READY_IDLE_STABLE_MS = 400;
|
|
50
|
+
/**
|
|
51
|
+
* How long a control slash command (`/model`, `/effort`) waits for the terminal to accept
|
|
52
|
+
* input before giving up.
|
|
53
|
+
*
|
|
54
|
+
* Bounded by the same reasoning as a stall: if the TUI has not shown an input box in that
|
|
55
|
+
* long, waiting further buys nothing and the caller deserves to hear that the setting did
|
|
56
|
+
* not apply.
|
|
57
|
+
*/
|
|
58
|
+
const CONTROL_COMMAND_MAX_WAIT_MS = 60000;
|
|
59
|
+
/** Poll interval while waiting for the input box. Cheap: it only reads cached state. */
|
|
60
|
+
const CONTROL_COMMAND_POLL_MS = 100;
|
|
61
|
+
/**
|
|
62
|
+
* Absolute ceiling on waiting for a busy terminal to go idle. Only reached while the TUI is
|
|
63
|
+
* actively telling us it is working, so it is a backstop against a wedged agent, not a boot
|
|
64
|
+
* budget. Matches the turn-stall backstop deliberately: both answer "this terminal claims to
|
|
65
|
+
* be working but never finished".
|
|
66
|
+
*/
|
|
67
|
+
const READY_BUSY_MAX_WAIT_MS = 10 * 60000;
|
|
68
|
+
/** The TUI prints this in its footer for exactly as long as a turn is running. */
|
|
69
|
+
const BUSY_MARKER = "esc to interrupt";
|
|
70
|
+
/**
|
|
71
|
+
* Footer fragments that only render when the input box is accepting typing. Matched against
|
|
72
|
+
* the rolling tail, and only believed when {@link BUSY_MARKER} is absent, because the busy
|
|
73
|
+
* footer contains some of these too (`… · esc to interrupt · ← for agents`).
|
|
74
|
+
*/
|
|
75
|
+
const IDLE_PROMPT_MARKERS = [
|
|
76
|
+
"for agents",
|
|
77
|
+
"for shortcuts",
|
|
78
|
+
"bypass permissions",
|
|
79
|
+
"shift+tab to cycle",
|
|
80
|
+
"for commands",
|
|
81
|
+
];
|
|
82
|
+
const TURN_END_IDLE_MS = 700;
|
|
83
|
+
/**
|
|
84
|
+
* How long to wait for the TUI to react to a slash command before giving up on it. Short:
|
|
85
|
+
* a command either renders immediately or was swallowed, and pressing Enter again would
|
|
86
|
+
* run it twice.
|
|
87
|
+
*/
|
|
88
|
+
const SLASH_COMMAND_ECHO_WAIT_MS = 3000;
|
|
89
|
+
/**
|
|
90
|
+
* Echo-verified delivery.
|
|
91
|
+
*
|
|
92
|
+
* A claude TUI that is still parsing a large resumed transcript PAINTS its idle footer
|
|
93
|
+
* before its input loop is live, and in that state it DISCARDS keystrokes: typing produces
|
|
94
|
+
* no echo at all. Observed 2026-08-03 on a 215MB session: attach at 13:03:00, idle footer
|
|
95
|
+
* by 13:03:04, prompt typed plus five Enters, zero echo in the recording, no user line,
|
|
96
|
+
* message lost -- while every successful delivery in the same recordings shows the typed
|
|
97
|
+
* text echoed back. Echo is therefore the one signal that distinguishes "idle" from
|
|
98
|
+
* "idle-looking but deaf", and its absence also proves the keystrokes were discarded,
|
|
99
|
+
* which is exactly what makes retyping safe rather than a duplication risk.
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* How much of the typed text must be found in the terminal output for it to count as an
|
|
103
|
+
* echo. Byte-counting was not enough: a freshly spawned claude under load keeps painting
|
|
104
|
+
* boot output (splash, MCP connects) after the readiness gate settles, and those bytes
|
|
105
|
+
* passed the old any-bytes check while the input loop was still dead -- three scheduled
|
|
106
|
+
* spawns failed that way in five minutes on a loaded box, 2026-08-03. Boot output never
|
|
107
|
+
* contains the user's message; a real input-box echo always does.
|
|
108
|
+
*/
|
|
109
|
+
/** How much of the message identifies it in a transcript record. */
|
|
110
|
+
const RECEIPT_FINGERPRINT_CHARS = 64;
|
|
111
|
+
/** Below this length a fingerprint must match a record WHOLE, never as a substring. */
|
|
112
|
+
const RECEIPT_EXACT_MATCH_BELOW_CHARS = 12;
|
|
113
|
+
/** How long one delivery attempt waits for its transcript receipt before retrying. */
|
|
114
|
+
const RECEIPT_WINDOW_MS = 4000;
|
|
115
|
+
/** A queued message can only land when the running turn ends, so wait far longer for it. */
|
|
116
|
+
const QUEUED_RECEIPT_WINDOW_MS = 10 * 60000;
|
|
117
|
+
/**
|
|
118
|
+
* The TUI's own words for "your message is in the queue".
|
|
119
|
+
*
|
|
120
|
+
* A message typed while a turn is running is QUEUED by claude, and a queued message
|
|
121
|
+
* appends NO `user` record until the running turn ends -- so the one delivery oracle
|
|
122
|
+
* (receiptSince) cannot fire, by construction, for minutes at a time. The old test for
|
|
123
|
+
* "is a turn running" was `lastTranscriptAt > attemptStartedAt`, i.e. "did the transcript
|
|
124
|
+
* grow during my 4s window". That is a proxy, and it reads FALSE whenever the running
|
|
125
|
+
* turn is simply quiet for four seconds (thinking, or one slow tool call). When it reads
|
|
126
|
+
* false the loop concludes the terminal is deaf and retypes.
|
|
127
|
+
*
|
|
128
|
+
* Retyping a QUEUED message is not idempotent: the composer is empty (the text already
|
|
129
|
+
* left it), so Ctrl+U clears nothing and the retype either stacks a second queued copy or
|
|
130
|
+
* concatenates onto text the clear failed to kill. Observed live 2026-08-20 on session
|
|
131
|
+
* 0b1ec622, four retries logged at 18:28:13 / 18:29:59 / 18:30:06 / 18:30:14 / 18:30:27,
|
|
132
|
+
* producing these transcript records:
|
|
133
|
+
*
|
|
134
|
+
* mint another xpmint another xp
|
|
135
|
+
* a new one with another cpf: 76797678153, caixaa new one with another cpf: ...
|
|
136
|
+
*
|
|
137
|
+
* The terminal was stating the answer the whole time. Paseo's own turn heartbeat captured
|
|
138
|
+
* it at 18:30:15: `... Press up to edit queued messages - esc to interrupt`. This reads
|
|
139
|
+
* that footer instead of inferring it from file growth.
|
|
140
|
+
*/
|
|
141
|
+
const QUEUED_FOOTER_RE = /Press up to edit queued messages/i;
|
|
142
|
+
/** How much of the tail to scan for the footer. It is repainted on every frame. */
|
|
143
|
+
const QUEUED_FOOTER_SCAN_CHARS = 2000;
|
|
144
|
+
/** Backoff between retype attempts against a deaf terminal. */
|
|
145
|
+
const DEAF_RETRY_INITIAL_MS = 2000;
|
|
146
|
+
const DEAF_RETRY_MAX_MS = 60000;
|
|
147
|
+
const TERMINAL_STOP_REASONS = new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
148
|
+
/**
|
|
149
|
+
* Liveness backstop: how long an in-flight turn may append NOTHING to the transcript
|
|
150
|
+
* before PtyQuery gives up and synthesizes a failed `result`.
|
|
151
|
+
*
|
|
152
|
+
* Without this the PTY path has no liveness detection at all, and a claude-side stall is
|
|
153
|
+
* indistinguishable from a long turn: the agent sits at `status: running` forever with no
|
|
154
|
+
* output, no error and no timeout. Observed 2026-07-31: an agent accepted a prompt, hit a
|
|
155
|
+
* provider auth failure, and stayed "running" for 2h33m emitting nothing, because
|
|
156
|
+
* - the PTY's own output is unsubscribed once the readiness gate settles, so whatever the
|
|
157
|
+
* TUI prints (`API Error 401 · Retrying in 32s`, …) is never seen, and
|
|
158
|
+
* - `scheduleTurnEndCheck()` is only ever armed from `onTranscriptMessage()`, so silence
|
|
159
|
+
* never arms anything, and `maybeCompleteTurn()` returns early unless it already saw a
|
|
160
|
+
* terminal `stop_reason`.
|
|
161
|
+
* `TURN_END_IDLE_MS` is a 700ms turn-BOUNDARY debounce, not a stall detector.
|
|
162
|
+
*
|
|
163
|
+
* Deliberately generous: a legitimate turn can be quiet for a long time (one slow tool call
|
|
164
|
+
* writes nothing until its `tool_result` lands), and a false positive marks a healthy turn
|
|
165
|
+
* failed. Ten minutes is far past any normal tool call while still bounded. Tune per-call
|
|
166
|
+
* with `stallTimeoutMs`; `0` disables the backstop entirely.
|
|
167
|
+
*/
|
|
168
|
+
const TURN_STALL_TIMEOUT_MS = 10 * 60000;
|
|
169
|
+
/**
|
|
170
|
+
* How much terminal text to keep for diagnosis. The TUI is the only place that says what
|
|
171
|
+
* claude is actually doing while it is quiet ("Compacting conversation…", a retry notice,
|
|
172
|
+
* a prompt), and PtyQuery otherwise unsubscribes from it once the readiness gate settles,
|
|
173
|
+
* so a stall investigation has nothing to read.
|
|
174
|
+
*
|
|
175
|
+
* A rolling tail rather than a live log on purpose: the TUI redraws constantly (spinner
|
|
176
|
+
* frames, cursor moves), so streaming it would flood the daemon log for no benefit. This
|
|
177
|
+
* keeps only the last few KB and emits it exactly once, when a turn is declared stalled.
|
|
178
|
+
*/
|
|
179
|
+
const TERMINAL_TAIL_MAX_CHARS = 4000;
|
|
180
|
+
/** Chars of the tail to include in the user-facing stall message; the rest goes to the log. */
|
|
181
|
+
const TERMINAL_TAIL_EXCERPT_CHARS = 300;
|
|
182
|
+
/**
|
|
183
|
+
* Telemetry. The PTY path had none: a session that went quiet left no record of what it had
|
|
184
|
+
* been doing, and four distinct failure modes (prompt never submitted, stall after a
|
|
185
|
+
* compaction, never finished booting, genuinely waiting on the model) were indistinguishable
|
|
186
|
+
* from each other and from a slow turn. Each of the three below exists to separate them.
|
|
187
|
+
*/
|
|
188
|
+
/** Heartbeat cadence while a turn is outstanding. Cheap: one line per turn per interval. */
|
|
189
|
+
const LIVENESS_HEARTBEAT_MS = 30000;
|
|
190
|
+
/**
|
|
191
|
+
* The claude TUI's post-interrupt modal. A cancelled turn leaves the terminal sitting on
|
|
192
|
+
* `Interrupted · What should Claude do instead?`, and in that state Enter answers the dialog
|
|
193
|
+
* instead of submitting a turn, so every subsequent prompt is swallowed whole and the retry
|
|
194
|
+
* loop in submitTurn cannot win. Observed live 2026-08-01: a turn was cancelled at 16:56:08
|
|
195
|
+
* and the next message failed all five submit attempts 35s later.
|
|
196
|
+
*
|
|
197
|
+
* Matched against the terminal tail rather than guessed at, and acted on only while the
|
|
198
|
+
* sighting is recent, because sending ESC blindly would interrupt a healthy running turn.
|
|
199
|
+
*/
|
|
200
|
+
const INTERRUPT_PROMPT_MARKER = "what should claude do instead";
|
|
201
|
+
/**
|
|
202
|
+
* The CLI's large-session resume dialog. Resuming a session past a usage threshold shows
|
|
203
|
+
*
|
|
204
|
+
* This session is Nd old and NNNk tokens. ...We recommend resuming from a summary.
|
|
205
|
+
* > 1. Resume from summary (recommended) <- PRE-SELECTED
|
|
206
|
+
* 2. Resume full session as-is
|
|
207
|
+
* 3. Don't ask me again
|
|
208
|
+
*
|
|
209
|
+
* "Resume from summary" runs /compact, and confirming it is recorded in the transcript as
|
|
210
|
+
* a USER-TYPED /compact (entrypoint cli, userType external) -- indistinguishable from real
|
|
211
|
+
* input. Left unhandled, paseo's flow walks straight into it: the dialog is quiet, so the
|
|
212
|
+
* readiness gate settles; the user's message is typed into the menu and eaten; and the
|
|
213
|
+
* submit Enter confirms the pre-selected compaction. Proven by isolated replication
|
|
214
|
+
* 2026-08-03: two sessions compacted from a single Enter with paseo not involved, a third
|
|
215
|
+
* resumed intact when option 2 was selected instead.
|
|
216
|
+
*
|
|
217
|
+
* The answer is option 2: the user asked to talk to this session, so resume it whole.
|
|
218
|
+
* Selecting it takes one Down (to move off option 1) and one Enter.
|
|
219
|
+
*/
|
|
220
|
+
const RESUME_DIALOG_MARKER = "resume from summary";
|
|
221
|
+
/** How long a sighting stays actionable; mirrors INTERRUPT_PROMPT_TTL_MS reasoning. */
|
|
222
|
+
const RESUME_DIALOG_TTL_MS = 5 * 60000;
|
|
223
|
+
/** How long a sighting stays actionable. Beyond this, assume the modal is long gone. */
|
|
224
|
+
const INTERRUPT_PROMPT_TTL_MS = 5 * 60000;
|
|
225
|
+
/**
|
|
226
|
+
* Full-session terminal recording, written next to the daemon log under
|
|
227
|
+
* `$PASEO_HOME/pty-recordings/<sessionId>.log`.
|
|
228
|
+
*
|
|
229
|
+
* The 4KB in-memory tail only survives to the next stall and holds seconds of history once
|
|
230
|
+
* the spinner is running; a session that failed at boot produced 579 chars total. A file
|
|
231
|
+
* keeps the whole run, so a failure can be read backwards afterwards instead of inferred.
|
|
232
|
+
* Capped and truncated rather than rotated: this is diagnostic data, the recent past is what
|
|
233
|
+
* matters, and an unbounded file on a long-running agent is its own incident.
|
|
234
|
+
*/
|
|
235
|
+
/** Tabs to try before giving up on finding the picker's Submit tab. */
|
|
236
|
+
const DIALOG_SUBMIT_MAX_TABS = 12;
|
|
237
|
+
const RECORDING_MAX_BYTES = 4 * 1024 * 1024;
|
|
238
|
+
/** Keep this much when the cap is hit, so a truncation never leaves an empty file. */
|
|
239
|
+
const RECORDING_KEEP_BYTES = 1024 * 1024;
|
|
240
|
+
/**
|
|
241
|
+
* Monotonic milliseconds for every liveness comparison here.
|
|
242
|
+
*
|
|
243
|
+
* Never Date.now(): a stepping wall clock silently breaks all of them. Measured on this
|
|
244
|
+
* machine, the clock oscillated +/-48.6s roughly six times a minute, which made the
|
|
245
|
+
* readiness gate believe its 60s budget had elapsed after ~11s and type the user's prompt
|
|
246
|
+
* into a terminal that was still working, losing the message. The tell was an overshoot of
|
|
247
|
+
* 3641ms on a loop that ticks every 500ms: real elapse cannot overshoot by more than one
|
|
248
|
+
* tick, a clock step can.
|
|
249
|
+
*/
|
|
250
|
+
function monotonicNowMs() {
|
|
251
|
+
return performance.now();
|
|
252
|
+
}
|
|
253
|
+
export class PtyQuery {
|
|
254
|
+
constructor(opts) {
|
|
255
|
+
/** One queued notice per message, not one per retry. Reset by deliverPrompt(). */
|
|
256
|
+
this.queuedNoticeSent = false;
|
|
257
|
+
// output queue → async iterator
|
|
258
|
+
this.outQueue = [];
|
|
259
|
+
this.outResolvers = [];
|
|
260
|
+
this.done = false;
|
|
261
|
+
this.offReadyData = null;
|
|
262
|
+
/** Unsubscribes the transport exit tap. Teardown MUST call this. */
|
|
263
|
+
this.offExitTap = null;
|
|
264
|
+
/** Set once the agent process has exited; the two remaining "still running" claims read it. */
|
|
265
|
+
this.transportExited = null;
|
|
266
|
+
/**
|
|
267
|
+
* Cancels the readiness gate and its timers.
|
|
268
|
+
*
|
|
269
|
+
* Teardown MUST call this. A query closed before the gate settles used to leave its
|
|
270
|
+
* deadline interval running, so a session that attached and detached in the same
|
|
271
|
+
* millisecond still logged "readiness gate hit its timeout" a full minute later, warning
|
|
272
|
+
* about a terminal nobody was waiting on and leaking a timer per closed session.
|
|
273
|
+
*/
|
|
274
|
+
this.cancelReadiness = null;
|
|
275
|
+
// turn-end tracking
|
|
276
|
+
this.turnEndTimer = null;
|
|
277
|
+
this.lastStopReason = null;
|
|
278
|
+
this.lastUsage = null;
|
|
279
|
+
this.turnStartedAt = 0;
|
|
280
|
+
this.numTurns = 0;
|
|
281
|
+
this.stallTimer = null;
|
|
282
|
+
this.turnInFlight = false;
|
|
283
|
+
// terminal tail (see TERMINAL_TAIL_MAX_CHARS)
|
|
284
|
+
this.terminalTail = "";
|
|
285
|
+
this.offTerminalTap = null;
|
|
286
|
+
// telemetry
|
|
287
|
+
this.startedAt = monotonicNowMs();
|
|
288
|
+
this.recordingPath = null;
|
|
289
|
+
this.recordingBytes = 0;
|
|
290
|
+
this.ptyBytesTotal = 0;
|
|
291
|
+
this.ptyBytesAtLastBeat = 0;
|
|
292
|
+
this.lastMeaningfulLine = "";
|
|
293
|
+
this.lastTranscriptAt = 0;
|
|
294
|
+
this.heartbeatTimer = null;
|
|
295
|
+
this.readySettledBy = null;
|
|
296
|
+
/** Last time the TUI footer said a turn was running. 0 = never seen. */
|
|
297
|
+
this.lastBusyAt = 0;
|
|
298
|
+
/**
|
|
299
|
+
* Serializes everything that TYPES into the terminal.
|
|
300
|
+
*
|
|
301
|
+
* Prompt delivery and control slash commands both write to the same single input line, and
|
|
302
|
+
* before this they ran on independent async paths with nothing ordering them.
|
|
303
|
+
*/
|
|
304
|
+
this.writeGate = Promise.resolve();
|
|
305
|
+
/** Last sighting of the large-session resume dialog. 0 = never seen. */
|
|
306
|
+
this.resumeDialogAt = 0;
|
|
307
|
+
/** Set when the CLI rejected the just-submitted command; carries the CLI's own reply. */
|
|
308
|
+
this.rejectedCommandNotice = null;
|
|
309
|
+
/** Interactive TUI dialog awaiting the user (AskUserQuestion / plan approval), if any. */
|
|
310
|
+
this.pendingDialog = null;
|
|
311
|
+
/**
|
|
312
|
+
* Raw capture window for echo verification. The rolling tail deliberately drops lines
|
|
313
|
+
* with fewer than three letters as spinner noise, which also swallows the echo of a
|
|
314
|
+
* short message: "> go" never reaches the tail, so verifying against the tail held a
|
|
315
|
+
* two-letter message forever. While this is non-null, appendTerminalTail mirrors every
|
|
316
|
+
* stripped chunk here unfiltered.
|
|
317
|
+
*/
|
|
318
|
+
this.echoProbe = null;
|
|
319
|
+
this.interruptPromptAt = 0;
|
|
320
|
+
this.transport = opts.transport;
|
|
321
|
+
this.input = opts.input;
|
|
322
|
+
this.sessionId = opts.sessionId;
|
|
323
|
+
this.transcriptPath = opts.transcriptPath;
|
|
324
|
+
this.logger = opts.logger;
|
|
325
|
+
this.stallTimeoutMs = opts.stallTimeoutMs ?? TURN_STALL_TIMEOUT_MS;
|
|
326
|
+
this.onDialogChange = opts.onDialogChange ?? null;
|
|
327
|
+
this.onNotice = opts.onNotice ?? null;
|
|
328
|
+
this.reader = new TranscriptSdkReader({
|
|
329
|
+
transcriptPath: opts.transcriptPath,
|
|
330
|
+
onMessage: (m) => this.onTranscriptMessage(m),
|
|
331
|
+
...(opts.onUserPrompt ? { onUserPrompt: opts.onUserPrompt } : {}),
|
|
332
|
+
});
|
|
333
|
+
// Keep a rolling tail of terminal output. Registered BEFORE the readiness tap below so
|
|
334
|
+
// that looksIdle() sees the CURRENT chunk: handlers fire in registration order, and
|
|
335
|
+
// with the order reversed an idle prompt in the final frame was never noticed, because
|
|
336
|
+
// readiness inspected a tail that did not yet contain it. This one lives for the whole
|
|
337
|
+
// session so a stall has something to explain itself with. Chunks arrive already redacted (PtyTransport.onData
|
|
338
|
+
// runs redactChunk), so nothing further is needed here.
|
|
339
|
+
this.offTerminalTap = this.transport.onData((chunk) => this.appendTerminalTail(chunk));
|
|
340
|
+
// Learn about the agent process dying the moment it happens. Without this tap the only
|
|
341
|
+
// thing that ever noticed was the stall backstop, ten minutes later, and it then told the
|
|
342
|
+
// user the session was "still running and may recover" - of a process that had already
|
|
343
|
+
// printed its farewell banner and exited. The stall timer is for a live agent that is
|
|
344
|
+
// slow; it is the wrong instrument for a dead one.
|
|
345
|
+
this.offExitTap = this.transport.onExit((code, signal) => this.onTransportExit(code, signal));
|
|
346
|
+
// The executor stays trivial and settlement lives outside it, so there is exactly one
|
|
347
|
+
// resolve() call in the file. Timers and handlers below all funnel through settle(),
|
|
348
|
+
// which is guarded by `settled`; expressing that inside the executor tripped
|
|
349
|
+
// promise/no-multiple-resolved, and the rule is right that multiple resolve paths in an
|
|
350
|
+
// executor are hard to audit.
|
|
351
|
+
let resolveReady = () => { };
|
|
352
|
+
this.readyPromise = new Promise((resolve) => {
|
|
353
|
+
resolveReady = resolve;
|
|
354
|
+
});
|
|
355
|
+
let settled = false;
|
|
356
|
+
// Single resolve site. Both the normal outcomes and teardown funnel through here, so
|
|
357
|
+
// the promise provably resolves once no matter which timer or path gets there first.
|
|
358
|
+
const settle = (by) => {
|
|
359
|
+
if (settled)
|
|
360
|
+
return;
|
|
361
|
+
settled = true;
|
|
362
|
+
this.readySettledBy = by === "cancelled" ? null : by;
|
|
363
|
+
this.offReadyData?.();
|
|
364
|
+
clearTimeout(quietTimer ?? undefined);
|
|
365
|
+
clearTimeout(idleTimer ?? undefined);
|
|
366
|
+
clearInterval(deadlineTimer);
|
|
367
|
+
// A torn-down query never had a readiness problem, so it reports nothing.
|
|
368
|
+
if (by !== "cancelled") {
|
|
369
|
+
// Settling on the timeout means claude never produced output that looked like a
|
|
370
|
+
// booted TUI, so the prompt about to be typed may go into a terminal that is not
|
|
371
|
+
// listening yet and be lost outright. Loud on purpose.
|
|
372
|
+
const level = by === "timeout" ? "warn" : "debug";
|
|
373
|
+
this.logger[level]({
|
|
374
|
+
sessionId: this.sessionId,
|
|
375
|
+
settledBy: by,
|
|
376
|
+
elapsedMs: monotonicNowMs() - this.startedAt,
|
|
377
|
+
ptyBytes: this.ptyBytesTotal,
|
|
378
|
+
sawBusyMarker: this.lastBusyAt > 0,
|
|
379
|
+
lastLine: this.lastMeaningfulLine.slice(-120),
|
|
380
|
+
}, by === "timeout"
|
|
381
|
+
? "PtyQuery: readiness gate hit its timeout, claude may not be accepting input yet"
|
|
382
|
+
: "PtyQuery: readiness settled on terminal output");
|
|
383
|
+
}
|
|
384
|
+
resolveReady();
|
|
385
|
+
};
|
|
386
|
+
const finish = settle;
|
|
387
|
+
this.cancelReadiness = () => settle("cancelled");
|
|
388
|
+
let quietTimer = null;
|
|
389
|
+
let idleTimer = null;
|
|
390
|
+
this.offReadyData = this.transport.onData(() => {
|
|
391
|
+
// Two independent paths to ready, because a PtyQuery attaches to a LONG-LIVED
|
|
392
|
+
// terminal, not only to a fresh boot (observed 2026-08-01: nine attach/detach
|
|
393
|
+
// cycles on one session's PTY).
|
|
394
|
+
//
|
|
395
|
+
// - quiet: nothing has been painted for a while. Correct for a cold boot.
|
|
396
|
+
// - idle-prompt: the footer positively shows an input box and no busy marker.
|
|
397
|
+
//
|
|
398
|
+
// Quiet ALONE is wrong on re-attach: if the agent is mid-turn the spinner repaints
|
|
399
|
+
// at a couple of KB/s forever, so "went quiet" can never fire and the gate used to
|
|
400
|
+
// fall through to its timeout and type into a working terminal. That was the real
|
|
401
|
+
// cause of the lost prompts, and it is why absence-of-output cannot be the only
|
|
402
|
+
// signal here.
|
|
403
|
+
if (quietTimer)
|
|
404
|
+
clearTimeout(quietTimer);
|
|
405
|
+
quietTimer = setTimeout(() => finish("quiet"), READY_QUIET_SETTLE_MS);
|
|
406
|
+
quietTimer.unref?.();
|
|
407
|
+
if (this.looksIdle()) {
|
|
408
|
+
// Hold the idle read briefly: the footer repaints mid-turn and a single frame
|
|
409
|
+
// without the busy marker is a flicker, not idleness.
|
|
410
|
+
if (!idleTimer) {
|
|
411
|
+
idleTimer = setTimeout(() => {
|
|
412
|
+
if (this.looksIdle())
|
|
413
|
+
finish("idle-prompt");
|
|
414
|
+
idleTimer = null;
|
|
415
|
+
}, READY_IDLE_STABLE_MS);
|
|
416
|
+
idleTimer.unref?.();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else if (idleTimer) {
|
|
420
|
+
clearTimeout(idleTimer);
|
|
421
|
+
idleTimer = null;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
// The deadline is conditional on evidence, not a flat budget. Only a terminal showing
|
|
425
|
+
// NO sign of life gets cut off at the short budget.
|
|
426
|
+
//
|
|
427
|
+
// Liveness is "has this PTY ever produced output", NOT "did the busy footer repaint
|
|
428
|
+
// recently". An earlier version keyed on the footer and timed out anyway on a session
|
|
429
|
+
// that was plainly working (60017ms, 78991 bytes, sawBusyMarker true but outside the
|
|
430
|
+
// window). Measured across captured sessions, the gap between footer repaints has a
|
|
431
|
+
// median of 37.7s and a p90 of 483s, so no recency window is defensible.
|
|
432
|
+
//
|
|
433
|
+
// Reaching this branch at all already proves the terminal is streaming: the quiet
|
|
434
|
+
// timer above resolves readiness after 1.5s of silence, so if we are still un-ready
|
|
435
|
+
// the quiet timer must be getting reset continuously. A genuinely dead terminal never
|
|
436
|
+
// fires onData, never arms the quiet timer, and never records a byte, which is exactly
|
|
437
|
+
// the case the short budget exists for.
|
|
438
|
+
const deadlineTimer = setInterval(() => {
|
|
439
|
+
const waited = monotonicNowMs() - this.startedAt;
|
|
440
|
+
if (this.ptyBytesTotal > 0) {
|
|
441
|
+
if (waited >= READY_BUSY_MAX_WAIT_MS)
|
|
442
|
+
finish("timeout");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (waited >= READY_MAX_WAIT_MS)
|
|
446
|
+
finish("timeout");
|
|
447
|
+
}, 500);
|
|
448
|
+
deadlineTimer.unref?.();
|
|
449
|
+
this.openRecording();
|
|
450
|
+
if (opts.initMessage)
|
|
451
|
+
this.emit(opts.initMessage);
|
|
452
|
+
this.reader.start();
|
|
453
|
+
void this.runInputLoop();
|
|
454
|
+
}
|
|
455
|
+
// ---- output queue / AsyncGenerator ----------------------------------------
|
|
456
|
+
emit(message) {
|
|
457
|
+
if (this.done)
|
|
458
|
+
return;
|
|
459
|
+
const resolve = this.outResolvers.shift();
|
|
460
|
+
if (resolve) {
|
|
461
|
+
resolve({ value: message, done: false });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.outQueue.push(message);
|
|
465
|
+
}
|
|
466
|
+
next() {
|
|
467
|
+
const queued = this.outQueue.shift();
|
|
468
|
+
if (queued !== undefined)
|
|
469
|
+
return Promise.resolve({ value: queued, done: false });
|
|
470
|
+
if (this.done)
|
|
471
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
472
|
+
return new Promise((resolve) => this.outResolvers.push(resolve));
|
|
473
|
+
}
|
|
474
|
+
return() {
|
|
475
|
+
this.finish();
|
|
476
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
477
|
+
}
|
|
478
|
+
throw(error) {
|
|
479
|
+
this.finish();
|
|
480
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
481
|
+
}
|
|
482
|
+
[Symbol.asyncIterator]() {
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
finish() {
|
|
486
|
+
if (this.done)
|
|
487
|
+
return;
|
|
488
|
+
this.done = true;
|
|
489
|
+
const hadDialog = this.pendingDialog !== null;
|
|
490
|
+
this.pendingDialog = null;
|
|
491
|
+
if (hadDialog)
|
|
492
|
+
this.notifyDialogChange();
|
|
493
|
+
this.cancelReadiness?.();
|
|
494
|
+
if (this.turnEndTimer)
|
|
495
|
+
clearTimeout(this.turnEndTimer);
|
|
496
|
+
this.turnEndTimer = null;
|
|
497
|
+
this.turnInFlight = false;
|
|
498
|
+
this.clearStallTimer();
|
|
499
|
+
this.offReadyData?.();
|
|
500
|
+
this.offTerminalTap?.();
|
|
501
|
+
this.offTerminalTap = null;
|
|
502
|
+
this.offExitTap?.();
|
|
503
|
+
this.offExitTap = null;
|
|
504
|
+
this.stopHeartbeat();
|
|
505
|
+
this.recordLine("--- PtyQuery detached ---");
|
|
506
|
+
try {
|
|
507
|
+
this.reader.stop();
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
/* ignore */
|
|
511
|
+
}
|
|
512
|
+
while (this.outResolvers.length > 0) {
|
|
513
|
+
this.outResolvers.shift()?.({ value: undefined, done: true });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ---- input → PTY ----------------------------------------------------------
|
|
517
|
+
async runInputLoop() {
|
|
518
|
+
// Gate the first prompt on claude being ready to accept input (boot + settle),
|
|
519
|
+
// otherwise early keystrokes are dropped by the TUI (observed in the spike).
|
|
520
|
+
await this.readyPromise;
|
|
521
|
+
try {
|
|
522
|
+
for await (const userMessage of this.input) {
|
|
523
|
+
if (this.done)
|
|
524
|
+
break;
|
|
525
|
+
// Inline images cannot be typed into a terminal, but the CLI agent can Read a
|
|
526
|
+
// file: stage each image to disk and reference it by path, the same serialization
|
|
527
|
+
// job toSdkUserMessage does for the SDK path.
|
|
528
|
+
const imageRefs = this.stageInlineImages(userMessage)
|
|
529
|
+
.map((p) => `[Attached image: ${p}]`)
|
|
530
|
+
.join(" ");
|
|
531
|
+
const text = [extractUserText(userMessage), imageRefs].filter(Boolean).join(" ").trim();
|
|
532
|
+
if (!text) {
|
|
533
|
+
// An empty prompt used to be dropped silently right here, AFTER the manager had
|
|
534
|
+
// already flipped the agent to "running": the sender stared at a spinner forever
|
|
535
|
+
// (observed live 2026-08-04, an image-only message from a 0.3.10 app whose
|
|
536
|
+
// composer also stripped the attachment). Nothing-deliverable is an answerable
|
|
537
|
+
// outcome, so answer it.
|
|
538
|
+
this.recordLine("--- message had no deliverable content; notifying sender ---");
|
|
539
|
+
this.emit(this.buildNothingDeliverableResult());
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (this.pendingDialog) {
|
|
543
|
+
// The TUI is showing a question/plan dialog: typed text would be eaten by it and
|
|
544
|
+
// pre-typing ESC would decline it. Either answer it (a reply that names an
|
|
545
|
+
// option) or tell the sender what is being asked - never type across it.
|
|
546
|
+
await this.answerPendingDialog(text);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
this.turnStartedAt = monotonicNowMs();
|
|
550
|
+
// Clear modals BEFORE typing, not after: text typed into either dialog is
|
|
551
|
+
// consumed by it, so recovering at submit time is already too late.
|
|
552
|
+
await this.answerResumeDialog();
|
|
553
|
+
await this.clearInterruptPrompt();
|
|
554
|
+
// Deliberately NOT normalized (e.g. lowercasing an auto-capitalized command
|
|
555
|
+
// token): paseo must never silently rewrite what the user typed. A wrong command is
|
|
556
|
+
// handled by relaying the CLI's own rejection -- including its "did you mean"
|
|
557
|
+
// suggestion -- back to the sender, which corrects every rejection cause honestly
|
|
558
|
+
// instead of hiding one narrow cause invisibly.
|
|
559
|
+
// Same gate as sendControlCommand: a control command must never interleave with a
|
|
560
|
+
// prompt on the shared input line.
|
|
561
|
+
const delivery = await this.runExclusive(() => this.deliverPrompt(text));
|
|
562
|
+
if (this.done)
|
|
563
|
+
break;
|
|
564
|
+
if (delivery === "rejected") {
|
|
565
|
+
// Not lost, refused: hand the CLI's own answer (which includes its "did you
|
|
566
|
+
// mean" suggestion) to the sender instead of a phantom running turn.
|
|
567
|
+
const notice = this.rejectedCommandNotice ?? "the command was not recognized.";
|
|
568
|
+
this.rejectedCommandNotice = null;
|
|
569
|
+
this.emit(this.buildCommandRejectedResult(notice));
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (delivery === "exited") {
|
|
573
|
+
// Dead, not deaf. `continue`, never `break`: the exit tap is what ends this query,
|
|
574
|
+
// and breaking here would instead leave the input channel undrained, so a resend
|
|
575
|
+
// would sit unread and the sender would watch a spinner - the symptom being fixed.
|
|
576
|
+
this.emit(this.buildExitedResult(this.transportExited?.code ?? null, this.transportExited?.signal ?? null));
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (delivery === "undelivered") {
|
|
580
|
+
// No transcript receipt for the whole budget: the message never became a turn,
|
|
581
|
+
// nothing was queued, and only the sender can decide what to do next.
|
|
582
|
+
this.recordLine("--- no delivery receipt within the budget; message not delivered ---");
|
|
583
|
+
this.emit(this.buildSubmitFailedResult());
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
// "unconfirmed" is the slash-command case: exactly one Enter was sent and the CLI
|
|
587
|
+
// reacted, but a slash command may append no user record to confirm against.
|
|
588
|
+
// Treat it as started and let the stall backstop notice if it never runs -- never
|
|
589
|
+
// press Enter again, which would run the command twice.
|
|
590
|
+
// The turn is now claude's to answer; start the liveness clock. submitTurn() has
|
|
591
|
+
// confirmed claude accepted it (a real user line landed), so from here on silence
|
|
592
|
+
// means a stall, not a slow start.
|
|
593
|
+
this.turnInFlight = true;
|
|
594
|
+
this.armStallTimer();
|
|
595
|
+
this.startHeartbeat();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
this.logger.warn({ err }, "PtyQuery input loop ended");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The message the user sees when the terminal would not take their prompt.
|
|
604
|
+
*
|
|
605
|
+
* Only reachable while the query is still live (teardown breaks out before this), so it
|
|
606
|
+
* always describes a message that genuinely did not land and that only the user can
|
|
607
|
+
* resend. Shaped as a normal result so it renders in the timeline rather than being
|
|
608
|
+
* buried in the daemon log.
|
|
609
|
+
*/
|
|
610
|
+
/** The CLI refused the command; relay its own reply so the sender can correct and resend. */
|
|
611
|
+
buildCommandRejectedResult(notice) {
|
|
612
|
+
const base = this.buildResult("submit_failed");
|
|
613
|
+
return {
|
|
614
|
+
...base,
|
|
615
|
+
subtype: "error_during_execution",
|
|
616
|
+
is_error: true,
|
|
617
|
+
errors: [`The agent's CLI rejected the command: ${notice} Nothing was run.`],
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Write each inline base64 image block to a temp file so the terminal agent can Read it
|
|
622
|
+
* by path. A staging failure is logged and skipped rather than thrown: the text part of
|
|
623
|
+
* the message (if any) must still be delivered.
|
|
624
|
+
*/
|
|
625
|
+
stageInlineImages(message) {
|
|
626
|
+
const content = message.message?.content;
|
|
627
|
+
if (!Array.isArray(content))
|
|
628
|
+
return [];
|
|
629
|
+
const staged = [];
|
|
630
|
+
for (const block of content) {
|
|
631
|
+
if (!block || typeof block !== "object")
|
|
632
|
+
continue;
|
|
633
|
+
const image = block;
|
|
634
|
+
if (image.type !== "image")
|
|
635
|
+
continue;
|
|
636
|
+
const source = image.source;
|
|
637
|
+
if (!source || source.type !== "base64" || !source.data)
|
|
638
|
+
continue;
|
|
639
|
+
try {
|
|
640
|
+
const dir = path.join(os.tmpdir(), "paseo-pty-images", this.sessionId);
|
|
641
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
642
|
+
const ext = IMAGE_MEDIA_TYPE_EXT[source.media_type ?? ""] ?? "png";
|
|
643
|
+
const file = path.join(dir, `image-${randomUUID().slice(0, 8)}.${ext}`);
|
|
644
|
+
fs.writeFileSync(file, Buffer.from(source.data, "base64"));
|
|
645
|
+
staged.push(file);
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
this.logger.warn({ err }, "PtyQuery: failed to stage an inline image for delivery");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return staged;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The message had no text and no stageable image, so there is literally nothing to type.
|
|
655
|
+
* Emitted instead of dropping the message, because the manager has already marked the
|
|
656
|
+
* agent running by the time the input loop sees it: silence here is a forever-spinner.
|
|
657
|
+
*/
|
|
658
|
+
buildNothingDeliverableResult() {
|
|
659
|
+
const base = this.buildResult("submit_failed");
|
|
660
|
+
return {
|
|
661
|
+
...base,
|
|
662
|
+
subtype: "error_during_execution",
|
|
663
|
+
is_error: true,
|
|
664
|
+
errors: [
|
|
665
|
+
`Your message had nothing the terminal transport could deliver: no text, and no ` +
|
|
666
|
+
`image it could stage to disk. Nothing was sent, so please resend it with text.` +
|
|
667
|
+
`${this.stillRunningClause()}`,
|
|
668
|
+
],
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* " The session itself is still running." -- but only when it is.
|
|
673
|
+
*
|
|
674
|
+
* The exit tap fixes the in-flight turn and the stall message. These two other results
|
|
675
|
+
* asserted the same thing unconditionally and are reachable after the process is gone, so
|
|
676
|
+
* they need the same correction. Wording matches buildExitedResult: no "aborted", and
|
|
677
|
+
* "status" rather than "code", so isAbortError() cannot swallow it and
|
|
678
|
+
* buildTurnFailedEvent() cannot scrape a phantom exit code out of it.
|
|
679
|
+
*/
|
|
680
|
+
stillRunningClause() {
|
|
681
|
+
if (!this.transportExited)
|
|
682
|
+
return " The session itself is still running.";
|
|
683
|
+
return (` The agent process has exited, so the session is NOT running any more and will not ` +
|
|
684
|
+
`recover on its own; the conversation is still on disk, recover it with ` +
|
|
685
|
+
`"paseo import ${this.sessionId}".`);
|
|
686
|
+
}
|
|
687
|
+
/** Tell the provider the dialog state changed; never let a listener throw into the loop. */
|
|
688
|
+
notifyDialogChange() {
|
|
689
|
+
if (!this.onDialogChange)
|
|
690
|
+
return;
|
|
691
|
+
try {
|
|
692
|
+
this.onDialogChange(this.pendingDialog ? { ...this.pendingDialog } : null);
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
this.logger.warn({ err }, "PtyQuery: dialog listener threw");
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/** The dialog the TUI is waiting on, if any. */
|
|
699
|
+
interactiveDialog() {
|
|
700
|
+
return this.pendingDialog ? { ...this.pendingDialog } : null;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Answer the pending dialog by option index (0-based), the way the app's question card
|
|
704
|
+
* does. Returns false when there is no dialog or the keystrokes never registered.
|
|
705
|
+
*/
|
|
706
|
+
async answerDialogByIndex(index) {
|
|
707
|
+
if (!this.pendingDialog || index < 0)
|
|
708
|
+
return false;
|
|
709
|
+
return await this.sendDialogAnswer(this.pendingDialog, index);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Answer a multi-question picker, one entry per question in on-screen order; a null
|
|
713
|
+
* leaves that question unanswered. The app's question card collects an answer for EVERY
|
|
714
|
+
* question, so collapsing them to a single index would throw away answers the user
|
|
715
|
+
* actually gave and submit those questions blank.
|
|
716
|
+
*/
|
|
717
|
+
async answerDialogByIndexes(indexes) {
|
|
718
|
+
const dialog = this.pendingDialog;
|
|
719
|
+
if (!dialog)
|
|
720
|
+
return false;
|
|
721
|
+
if (indexes.length <= 1) {
|
|
722
|
+
const only = indexes[0];
|
|
723
|
+
return typeof only === "number" && only >= 0 ? await this.answerDialogByIndex(only) : false;
|
|
724
|
+
}
|
|
725
|
+
this.recordLine(`--- answering ${indexes.length} questions ---`);
|
|
726
|
+
for (const index of indexes) {
|
|
727
|
+
if (this.done || !this.pendingDialog)
|
|
728
|
+
break;
|
|
729
|
+
if (typeof index === "number" && index >= 0) {
|
|
730
|
+
for (let i = 0; i < index && !this.done; i++) {
|
|
731
|
+
await this.transport.writeRaw?.("\u001b[B");
|
|
732
|
+
await delay(DIALOG_KEY_DELAY_MS);
|
|
733
|
+
}
|
|
734
|
+
// Enter checks this question and moves focus to the next one.
|
|
735
|
+
await this.transport.writeRaw?.("\r");
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// No answer for this one: Tab past it rather than picking whatever is highlighted.
|
|
739
|
+
await this.transport.writeRaw?.("\t");
|
|
740
|
+
}
|
|
741
|
+
await delay(DIALOG_KEY_DELAY_MS);
|
|
742
|
+
}
|
|
743
|
+
await this.reachSubmitTabAndConfirm();
|
|
744
|
+
const deadline = monotonicNowMs() + DIALOG_ANSWER_CONFIRM_MS;
|
|
745
|
+
while (this.pendingDialog && !this.done && monotonicNowMs() < deadline) {
|
|
746
|
+
await delay(250);
|
|
747
|
+
}
|
|
748
|
+
if (this.pendingDialog) {
|
|
749
|
+
this.recordLine("--- multi-question answer did not register ---");
|
|
750
|
+
this.notifyDialogDriveFailed();
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
this.turnStartedAt = monotonicNowMs();
|
|
754
|
+
this.turnInFlight = true;
|
|
755
|
+
this.armStallTimer();
|
|
756
|
+
this.startHeartbeat();
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Try to answer the pending TUI dialog with the user's reply. A reply that names an
|
|
761
|
+
* option (its 1-based number or its label) becomes Down-arrow presses + Enter, the same
|
|
762
|
+
* proven mechanism as the resume-dialog answerer. Anything else gets the question echoed
|
|
763
|
+
* back with the options, and nothing is typed - an unanswerable message must never turn
|
|
764
|
+
* into a silent decline.
|
|
765
|
+
*/
|
|
766
|
+
async answerPendingDialog(text) {
|
|
767
|
+
const dialog = this.pendingDialog;
|
|
768
|
+
if (!dialog)
|
|
769
|
+
return;
|
|
770
|
+
const index = matchDialogOption(text, dialog.options);
|
|
771
|
+
if (index !== null) {
|
|
772
|
+
const answered = await this.sendDialogAnswer(dialog, index);
|
|
773
|
+
if (!answered) {
|
|
774
|
+
this.emit(this.buildDialogPendingResult(dialog, true));
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
// The reply names no option. That is NOT a dead end: an AskUserQuestion picker always
|
|
779
|
+
// renders a free-text entry after the model's options, so free text has a real home
|
|
780
|
+
// in the dialog. Dropping it here is what made a typed answer look like it vanished.
|
|
781
|
+
if (dialog.name === "AskUserQuestion" && (await this.sendDialogFreeText(text)))
|
|
782
|
+
return;
|
|
783
|
+
this.recordLine(`--- reply does not name a ${dialog.name} option; echoing question ---`);
|
|
784
|
+
this.notify(`Your message was not delivered: the agent is waiting on a ${dialog.name === "ExitPlanMode" ? "plan-approval" : "question"} dialog and the reply matched no option. Reply with an option number to answer it.`);
|
|
785
|
+
this.emit(this.buildDialogPendingResult(dialog));
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Answer the question with free text by driving the picker's own "Type something" entry.
|
|
789
|
+
*
|
|
790
|
+
* The entry number is READ OFF THE SCREEN rather than computed as options.length + 1:
|
|
791
|
+
* the CLI owns that list and appends more than one trailing entry (a free-text one and a
|
|
792
|
+
* chat one, verified against claude 2.1.239), so an offset derived from the tool input
|
|
793
|
+
* would silently drift the day the CLI adds another. Returns false when the entry is not
|
|
794
|
+
* on screen or the answer never registers, so the caller can still tell the sender.
|
|
795
|
+
*/
|
|
796
|
+
async sendDialogFreeText(text) {
|
|
797
|
+
const entry = this.findFreeTextEntryNumber();
|
|
798
|
+
if (entry === null) {
|
|
799
|
+
this.recordLine("--- no free-text entry visible in the dialog ---");
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
this.recordLine(`--- answering with free text via entry ${entry} ---`);
|
|
803
|
+
for (let i = 1; i < entry && !this.done; i++) {
|
|
804
|
+
await this.transport.writeRaw?.("\u001b[B");
|
|
805
|
+
await delay(DIALOG_KEY_DELAY_MS);
|
|
806
|
+
}
|
|
807
|
+
await this.transport.writeRaw?.("\r");
|
|
808
|
+
await delay(DIALOG_KEY_DELAY_MS);
|
|
809
|
+
await this.transport.write(text);
|
|
810
|
+
await this.transport.writeRaw?.("\r");
|
|
811
|
+
// Same reason as sendDialogAnswer: with more than one question the picker advances
|
|
812
|
+
// rather than finishing, so without this the text lands, the wait below times out, and
|
|
813
|
+
// the sender is told the message was NOT delivered -- which invites them to send it
|
|
814
|
+
// twice.
|
|
815
|
+
await this.finishMultiQuestionDialog();
|
|
816
|
+
const deadline = monotonicNowMs() + DIALOG_ANSWER_CONFIRM_MS;
|
|
817
|
+
while (this.pendingDialog && !this.done && monotonicNowMs() < deadline) {
|
|
818
|
+
await delay(250);
|
|
819
|
+
}
|
|
820
|
+
if (this.pendingDialog) {
|
|
821
|
+
this.recordLine("--- free-text answer did not register ---");
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
this.turnStartedAt = monotonicNowMs();
|
|
825
|
+
this.turnInFlight = true;
|
|
826
|
+
this.armStallTimer();
|
|
827
|
+
this.startHeartbeat();
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* 1-based number of the picker's free-text entry, or null when it is not on screen.
|
|
832
|
+
* Whitespace is squeezed out before matching because a redrawing TUI captured through a
|
|
833
|
+
* PTY loses spaces inside a line ("4.Typesomething." is a real observed frame).
|
|
834
|
+
*/
|
|
835
|
+
findFreeTextEntryNumber() {
|
|
836
|
+
const squished = this.readTerminalTail().toLowerCase().replace(/\s+/g, "");
|
|
837
|
+
let found = null;
|
|
838
|
+
for (const match of squished.matchAll(/(\d+)\.typesomething/g)) {
|
|
839
|
+
const value = Number(match[1]);
|
|
840
|
+
if (Number.isFinite(value) && value > 0)
|
|
841
|
+
found = value;
|
|
842
|
+
}
|
|
843
|
+
return found;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The dialog was driven and did not settle. Say so.
|
|
847
|
+
*
|
|
848
|
+
* This exists because of the failure it was written for: tapping an option on the app's
|
|
849
|
+
* question card resolved the permission, drove real keystrokes, answered the first
|
|
850
|
+
* question, and then stalled -- and every caller DISCARDED the false this returns, so the
|
|
851
|
+
* card vanished, the chip kept spinning, and nothing anywhere told the sender. The typed
|
|
852
|
+
* path had a result message to fall back on; the card path had nothing at all. A notice
|
|
853
|
+
* at the point of failure covers both callers instead of one.
|
|
854
|
+
*/
|
|
855
|
+
notifyDialogDriveFailed() {
|
|
856
|
+
this.notify("Your answer was sent to the agent's question but the dialog did not close. " +
|
|
857
|
+
"It is still waiting, so try answering it again.");
|
|
858
|
+
}
|
|
859
|
+
/** Sender-facing line; never let a listener throw into the delivery loop. */
|
|
860
|
+
notify(text) {
|
|
861
|
+
try {
|
|
862
|
+
this.onNotice?.(text);
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
this.logger.warn({ err }, "PtyQuery: notice listener threw");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Drive the dialog's own keys: Down to the option, Enter to confirm. Confirmation is the
|
|
870
|
+
* tool_result landing in the transcript (which clears pendingDialog) - an optimistic flag
|
|
871
|
+
* here would record intent as fact.
|
|
872
|
+
*/
|
|
873
|
+
async sendDialogAnswer(dialog, index) {
|
|
874
|
+
this.recordLine(`--- answering ${dialog.name} with option ${index + 1} ---`);
|
|
875
|
+
for (let i = 0; i < index && !this.done; i++) {
|
|
876
|
+
await this.transport.writeRaw?.("\u001b[B");
|
|
877
|
+
await delay(DIALOG_KEY_DELAY_MS);
|
|
878
|
+
}
|
|
879
|
+
await this.transport.writeRaw?.("\r");
|
|
880
|
+
// Enter both answers the focused question AND advances to the next tab. With more
|
|
881
|
+
// than one question the dialog is therefore still up, and the tool_result cannot land
|
|
882
|
+
// until the whole set is submitted -- which is why a multi-question call used to time
|
|
883
|
+
// out here and report "answer did not register" (verified against claude 2.1.239).
|
|
884
|
+
await this.finishMultiQuestionDialog();
|
|
885
|
+
const deadline = monotonicNowMs() + DIALOG_ANSWER_CONFIRM_MS;
|
|
886
|
+
while (this.pendingDialog && !this.done && monotonicNowMs() < deadline) {
|
|
887
|
+
await delay(250);
|
|
888
|
+
}
|
|
889
|
+
if (this.pendingDialog) {
|
|
890
|
+
this.recordLine(`--- ${dialog.name} answer did not register ---`);
|
|
891
|
+
this.notifyDialogDriveFailed();
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
// Answer accepted: the turn is live again, restart the liveness clock.
|
|
895
|
+
this.turnStartedAt = monotonicNowMs();
|
|
896
|
+
this.turnInFlight = true;
|
|
897
|
+
this.armStallTimer();
|
|
898
|
+
this.startHeartbeat();
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Walk a multi-question picker to its Submit tab and confirm.
|
|
903
|
+
*
|
|
904
|
+
* Only the first question has an answer here (a reply, or the app card's choice), so the
|
|
905
|
+
* rest are submitted unanswered on purpose: the CLI supports exactly that and reports it
|
|
906
|
+
* to the model ("Beta went unanswered"), which is strictly better than the alternative
|
|
907
|
+
* this replaces, where the dialog stayed up until somebody cancelled it.
|
|
908
|
+
*
|
|
909
|
+
* Every step is screen-verified rather than counted, because the number of Tabs between
|
|
910
|
+
* the focused question and Submit depends on how many questions are left.
|
|
911
|
+
*/
|
|
912
|
+
async finishMultiQuestionDialog() {
|
|
913
|
+
const dialog = this.pendingDialog;
|
|
914
|
+
if (!dialog || dialog.optionsByQuestion.length <= 1)
|
|
915
|
+
return;
|
|
916
|
+
const unanswered = dialog.optionsByQuestion.length - 1;
|
|
917
|
+
this.recordLine(`--- multi-question dialog: submitting with ${unanswered} unanswered ---`);
|
|
918
|
+
if (await this.reachSubmitTabAndConfirm()) {
|
|
919
|
+
this.notify(`Submitted your answer to the first question. ${unanswered} other ` +
|
|
920
|
+
`${unanswered === 1 ? "question" : "questions"} in the same prompt went ` +
|
|
921
|
+
`unanswered - reply again to answer ${unanswered === 1 ? "it" : "them"}.`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Tab until the picker's review tab is on screen, then confirm the pre-selected
|
|
926
|
+
* "1. Submit answers". Screen-verified rather than counted: how many Tabs away Submit
|
|
927
|
+
* is depends on how many questions are left. Returns false if it never got there, so
|
|
928
|
+
* the caller does not claim a submission that did not happen.
|
|
929
|
+
*/
|
|
930
|
+
async reachSubmitTabAndConfirm() {
|
|
931
|
+
for (let attempt = 0; attempt < DIALOG_SUBMIT_MAX_TABS && !this.done; attempt++) {
|
|
932
|
+
await delay(DIALOG_KEY_DELAY_MS);
|
|
933
|
+
if (!this.pendingDialog)
|
|
934
|
+
return false;
|
|
935
|
+
if (this.onSubmitTab()) {
|
|
936
|
+
await this.transport.writeRaw?.("\r");
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
await this.transport.writeRaw?.("\t");
|
|
940
|
+
}
|
|
941
|
+
this.recordLine("--- never reached the Submit tab ---");
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
/** True when the picker is showing its review/Submit tab. */
|
|
945
|
+
onSubmitTab() {
|
|
946
|
+
const squished = this.readTerminalTail().toLowerCase().replace(/\s+/g, "");
|
|
947
|
+
return squished.includes("submitanswers") || squished.includes("readytosubmit");
|
|
948
|
+
}
|
|
949
|
+
/** The agent is waiting on a dialog; tell the sender what it asks and how to answer. */
|
|
950
|
+
buildDialogPendingResult(dialog, answerFailed = false) {
|
|
951
|
+
const base = this.buildResult("submit_failed");
|
|
952
|
+
const optionLines = dialog.options.length > 0
|
|
953
|
+
? ` Options: ${dialog.options.map((o, i) => `${i + 1}) ${o}`).join(" ")}.`
|
|
954
|
+
: "";
|
|
955
|
+
const preamble = answerFailed
|
|
956
|
+
? `The answer keystrokes did not register in the terminal, so the dialog is still up.`
|
|
957
|
+
: `Your message was NOT delivered because the agent is showing a ${dialog.name === "ExitPlanMode" ? "plan-approval" : "question"} dialog and is waiting for an answer.`;
|
|
958
|
+
return {
|
|
959
|
+
...base,
|
|
960
|
+
subtype: "error_during_execution",
|
|
961
|
+
is_error: true,
|
|
962
|
+
errors: [
|
|
963
|
+
`${preamble}${optionLines} Reply with just the option number (or its exact text) ` +
|
|
964
|
+
`to answer it.`,
|
|
965
|
+
],
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
buildSubmitFailedResult() {
|
|
969
|
+
const base = this.buildResult("submit_failed");
|
|
970
|
+
const excerpt = this.terminalTail.slice(-TERMINAL_TAIL_EXCERPT_CHARS).trim();
|
|
971
|
+
const tailLine = excerpt ? ` Last terminal output: ${JSON.stringify(excerpt)}.` : "";
|
|
972
|
+
return {
|
|
973
|
+
...base,
|
|
974
|
+
subtype: "error_during_execution",
|
|
975
|
+
is_error: true,
|
|
976
|
+
errors: [
|
|
977
|
+
`Your message did not reach the agent: the terminal would not accept it. ` +
|
|
978
|
+
`Nothing was sent, so please send it again.${this.stillRunningClause()}${tailLine}`,
|
|
979
|
+
],
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Press Enter to submit the composed prompt, and CONFIRM the turn actually started by
|
|
984
|
+
* watching for the USER LINE claude appends to <sid>.jsonl the instant it accepts a turn.
|
|
985
|
+
* During boot the TUI intermittently swallows the first Enter (a "what's new" notice /
|
|
986
|
+
* MCP-auth warning steals focus), so retry until it takes.
|
|
987
|
+
*
|
|
988
|
+
* The confirmation used to be `transcriptSize() > before`, which is any growth at all.
|
|
989
|
+
* That is not specific to the turn starting: claude writes `attachment`, `last-prompt`
|
|
990
|
+
* and other bookkeeping lines on its own schedule, so a swallowed Enter that happened to
|
|
991
|
+
* coincide with unrelated writes read as success. Observed in production 2026-08-01: a
|
|
992
|
+
* session logged 157 consecutive `attachment` lines and NOT ONE `user` line, yet
|
|
993
|
+
* submitTurn returned on the first attempt. The prompt stayed unsent in the TUI composer,
|
|
994
|
+
* paseo set turnInFlight and waited on a turn that had never begun, and the agent showed
|
|
995
|
+
* a spinner until the liveness backstop fired ten minutes later. The retry loop below was
|
|
996
|
+
* always right; the thing it was checking was not.
|
|
997
|
+
*/
|
|
998
|
+
/**
|
|
999
|
+
* Dismiss the post-interrupt modal if the terminal is showing one. ESC is only sent on a
|
|
1000
|
+
* recent sighting: on an idle prompt it is harmless, but on a live turn it would interrupt
|
|
1001
|
+
* real work, so a stale marker must never trigger it.
|
|
1002
|
+
*/
|
|
1003
|
+
async clearInterruptPrompt() {
|
|
1004
|
+
if (!this.interruptPromptAt)
|
|
1005
|
+
return;
|
|
1006
|
+
if (this.pendingDialog) {
|
|
1007
|
+
// ESC here would DECLINE the pending question/plan dialog - that is where every
|
|
1008
|
+
// phantom "User declined to answer" came from. The dialog branch of the input loop
|
|
1009
|
+
// owns this state; never clear modals across it.
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (monotonicNowMs() - this.interruptPromptAt > INTERRUPT_PROMPT_TTL_MS) {
|
|
1013
|
+
this.interruptPromptAt = 0;
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
this.logger.warn({ sessionId: this.sessionId, lastLine: this.lastMeaningfulLine.slice(-120) }, "PtyQuery: terminal is showing the post-interrupt prompt, dismissing it before submitting");
|
|
1017
|
+
this.recordLine("--- dismissing post-interrupt prompt before submit ---");
|
|
1018
|
+
for (let attempt = 0; attempt < 3 && this.interruptPromptAt && !this.done; attempt++) {
|
|
1019
|
+
const seenAt = this.interruptPromptAt;
|
|
1020
|
+
await this.transport.writeRaw?.("\u001b");
|
|
1021
|
+
await delay(250);
|
|
1022
|
+
// The marker only refreshes while the modal is still drawing; if it stopped, we are out.
|
|
1023
|
+
if (this.interruptPromptAt === seenAt) {
|
|
1024
|
+
this.interruptPromptAt = 0;
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
this.interruptPromptAt = 0;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Answer the large-session resume dialog with "Resume full session as-is".
|
|
1032
|
+
*
|
|
1033
|
+
* Must run BEFORE anything is typed: text typed while the menu is up is eaten by it, and
|
|
1034
|
+
* an Enter confirms the pre-selected "Resume from summary", compacting the session the
|
|
1035
|
+
* user was trying to talk to. Down moves the selection off option 1; Enter confirms
|
|
1036
|
+
* option 2. Only acts on a recent sighting, because on any other screen a stray
|
|
1037
|
+
* Down+Enter is at best a wasted keystroke and at worst an answer to a different menu.
|
|
1038
|
+
*/
|
|
1039
|
+
async answerResumeDialog() {
|
|
1040
|
+
if (!this.resumeDialogAt)
|
|
1041
|
+
return;
|
|
1042
|
+
if (monotonicNowMs() - this.resumeDialogAt > RESUME_DIALOG_TTL_MS) {
|
|
1043
|
+
this.resumeDialogAt = 0;
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
this.logger.warn({ sessionId: this.sessionId, lastLine: this.lastMeaningfulLine.slice(-120) }, "PtyQuery: large-session resume dialog is showing; selecting 'Resume full session as-is'");
|
|
1047
|
+
this.recordLine("--- resume dialog: selecting 'Resume full session as-is' ---");
|
|
1048
|
+
await this.transport.writeRaw?.("\u001b[B"); // Down: off the pre-selected compact option
|
|
1049
|
+
await delay(300);
|
|
1050
|
+
await this.transport.writeRaw?.("\r");
|
|
1051
|
+
await delay(500);
|
|
1052
|
+
this.resumeDialogAt = 0;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Type the prompt and confirm the terminal actually received it, retrying against a
|
|
1056
|
+
* deaf terminal with backoff.
|
|
1057
|
+
*
|
|
1058
|
+
* Returns true once an echo (or a transcript user line) confirms the keystrokes landed;
|
|
1059
|
+
* false only after the terminal stayed deaf for the whole {@link READY_BUSY_MAX_WAIT_MS}
|
|
1060
|
+
* ceiling. Retyping cannot double-deliver: a deaf terminal demonstrably discards input
|
|
1061
|
+
* (zero echo), and every iteration first checks whether a user line landed anyway.
|
|
1062
|
+
*/
|
|
1063
|
+
/**
|
|
1064
|
+
* Empty the input box before a retype. Ctrl+U kills the line the TUI is composing; a
|
|
1065
|
+
* pasted chip is dropped the same way. Best-effort: a transport that cannot take raw
|
|
1066
|
+
* keys just leaves the composer as it was.
|
|
1067
|
+
*/
|
|
1068
|
+
/**
|
|
1069
|
+
* Is the TUI currently showing its "queued messages" footer?
|
|
1070
|
+
*
|
|
1071
|
+
* This reads a CLI STATUS LINE, not a rendering of our own text. The distinction matters
|
|
1072
|
+
* because this file has (rightly) burned four incidents on trusting renderings as proof
|
|
1073
|
+
* of DELIVERY -- bytes, then echoed text, then a raw probe, then a paste chip. Delivery
|
|
1074
|
+
* is still proved by one thing only, a matching `user` record. The footer answers a
|
|
1075
|
+
* different and much cheaper question: is a turn running right now. The CLI is the
|
|
1076
|
+
* authority on that, and it prints the answer.
|
|
1077
|
+
*/
|
|
1078
|
+
queuedFooterVisible() {
|
|
1079
|
+
return QUEUED_FOOTER_RE.test(this.terminalTail.slice(-QUEUED_FOOTER_SCAN_CHARS));
|
|
1080
|
+
}
|
|
1081
|
+
/** Tell the sender their message is in line, once per message. */
|
|
1082
|
+
notifyQueued() {
|
|
1083
|
+
if (this.queuedNoticeSent)
|
|
1084
|
+
return;
|
|
1085
|
+
this.queuedNoticeSent = true;
|
|
1086
|
+
try {
|
|
1087
|
+
this.onNotice?.("Your message was accepted and is queued behind the turn this session is already " +
|
|
1088
|
+
"running. It will start as soon as that turn finishes.");
|
|
1089
|
+
}
|
|
1090
|
+
catch (err) {
|
|
1091
|
+
this.logger.debug({ err }, "PtyQuery: queued notice callback threw");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async clearComposer() {
|
|
1095
|
+
try {
|
|
1096
|
+
// Ctrl+E (end of line), Ctrl+U (kill to start), Ctrl+K (kill to end).
|
|
1097
|
+
//
|
|
1098
|
+
// Ctrl+U alone was not enough. In a readline-style buffer it kills from the CURSOR
|
|
1099
|
+
// to the start of the line: it empties the composer only when the cursor already
|
|
1100
|
+
// sits at the end, and it never touches what is after the cursor. Live 2026-08-20 a
|
|
1101
|
+
// retype landed ON TOP of text a Ctrl+U was supposed to have killed, which is how
|
|
1102
|
+
// session 0b1ec622 got these into its transcript:
|
|
1103
|
+
//
|
|
1104
|
+
// mint another xpmint another xp
|
|
1105
|
+
// a new one with another cpf: 76797678153, caixaa new one with another cpf: ...
|
|
1106
|
+
//
|
|
1107
|
+
// This sequence is cursor-position independent and idempotent: all three keys are
|
|
1108
|
+
// no-ops on an already empty composer, so it cannot damage the common case. Each
|
|
1109
|
+
// gets its own beat, because the TUI only processes keys as fast as its input loop
|
|
1110
|
+
// drains them.
|
|
1111
|
+
//
|
|
1112
|
+
// It makes the clear far more likely to succeed. It does NOT prove the composer is
|
|
1113
|
+
// empty, and this file deliberately has no way to prove that: every signal for it is
|
|
1114
|
+
// a rendering, and renderings are what four earlier incidents taught it not to
|
|
1115
|
+
// trust. The real protection against duplication is not retyping at all once the
|
|
1116
|
+
// text has demonstrably left the composer -- the queued-footer guard in
|
|
1117
|
+
// deliverPrompt(), which removes the case behind every duplicate observed so far.
|
|
1118
|
+
for (const key of ["\u0005", "\u0015", "\u000b"]) {
|
|
1119
|
+
await this.transport.writeRaw?.(key);
|
|
1120
|
+
await delay(60);
|
|
1121
|
+
}
|
|
1122
|
+
await delay(120);
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
this.logger.debug({ err }, "PtyQuery: could not clear the composer before retyping");
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Deliver a prompt and prove it, or say it failed. See docs/pty-delivery.md.
|
|
1130
|
+
*
|
|
1131
|
+
* One authority: a transcript user record matching the text. Nothing the terminal paints
|
|
1132
|
+
* gates this - echoed characters, paste chips, boot noise and footer hints are renderings,
|
|
1133
|
+
* and renderings changed with every CLI version while this code kept "discovering" new
|
|
1134
|
+
* ones (bytes -> text -> raw probe -> paste chip, four incidents, same defect each time).
|
|
1135
|
+
*
|
|
1136
|
+
* Retries are safe because the composer is cleared first, and a message QUEUED behind a
|
|
1137
|
+
* running turn is never retyped - that is the one way to send it twice.
|
|
1138
|
+
*/
|
|
1139
|
+
async deliverPrompt(text) {
|
|
1140
|
+
this.queuedNoticeSent = false;
|
|
1141
|
+
const fingerprint = PtyQuery.fingerprint(text);
|
|
1142
|
+
const watermark = this.transcriptSize();
|
|
1143
|
+
const deadline = monotonicNowMs() + READY_BUSY_MAX_WAIT_MS;
|
|
1144
|
+
let backoff = DEAF_RETRY_INITIAL_MS;
|
|
1145
|
+
let attempt = 0;
|
|
1146
|
+
while (!this.done && monotonicNowMs() < deadline) {
|
|
1147
|
+
// Re-checked every attempt, not once up front: the child can die between attempts and
|
|
1148
|
+
// this loop's budget is ten minutes. Retrying into a corpse is what produced the
|
|
1149
|
+
// "no delivery receipt; retrying" storms that ended at the 20-retry cap.
|
|
1150
|
+
if (this.transportExited)
|
|
1151
|
+
return "exited";
|
|
1152
|
+
attempt += 1;
|
|
1153
|
+
const attemptStartedAt = monotonicNowMs();
|
|
1154
|
+
// Empty the input box first: whatever an earlier attempt may have left there is
|
|
1155
|
+
// undelivered by definition (no receipt), and typing on top of it is how one message
|
|
1156
|
+
// became nine stacked paste blocks in production.
|
|
1157
|
+
if (attempt > 1)
|
|
1158
|
+
await this.clearComposer();
|
|
1159
|
+
try {
|
|
1160
|
+
await this.transport.write(text);
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
// The transport now refuses a write into a dead pty. Without this catch the rejection
|
|
1164
|
+
// escapes deliverPrompt into the input loop's handler, which only logs "input loop
|
|
1165
|
+
// ended" -- so the sender would watch a spinner and never be told anything.
|
|
1166
|
+
if (err instanceof PtyExitedError)
|
|
1167
|
+
return "exited";
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
const submitted = await this.submitTurn(text, watermark, fingerprint);
|
|
1171
|
+
if (submitted === "rejected")
|
|
1172
|
+
return "rejected";
|
|
1173
|
+
if (submitted === "delivered") {
|
|
1174
|
+
if (attempt > 1) {
|
|
1175
|
+
this.recordLine(`--- delivered on attempt ${attempt} ---`);
|
|
1176
|
+
}
|
|
1177
|
+
return "delivered";
|
|
1178
|
+
}
|
|
1179
|
+
// No receipt. Is a turn actually RUNNING (so our text is queued behind it), or was
|
|
1180
|
+
// the Enter simply swallowed? Two independent signals, ORed on purpose:
|
|
1181
|
+
//
|
|
1182
|
+
// - the TUI's own queued footer, which is the CLI stating the fact outright, and
|
|
1183
|
+
// - real SDK messages arriving during this attempt, the original heuristic.
|
|
1184
|
+
//
|
|
1185
|
+
// File growth alone cannot tell queued from swallowed: claude writes `attachment`
|
|
1186
|
+
// and other bookkeeping records on its own schedule, and treating that as "queued"
|
|
1187
|
+
// would sit here for ten minutes while the prompt was never submitted (the
|
|
1188
|
+
// 2026-08-01 bug, in reverse). But it also misses the common case, a running turn
|
|
1189
|
+
// that is quiet for the four-second receipt window, and the cost of missing it is a
|
|
1190
|
+
// retype that corrupts the queued prompt. The footer settles that case directly.
|
|
1191
|
+
if (this.queuedFooterVisible() || this.lastTranscriptAt > attemptStartedAt) {
|
|
1192
|
+
this.recordLine("--- no receipt yet; queued behind a running turn ---");
|
|
1193
|
+
// Say so where the sender can see it. Once per message, not once per poll.
|
|
1194
|
+
this.notifyQueued();
|
|
1195
|
+
const late = await pollUntil(() => this.receiptSince(watermark, fingerprint), QUEUED_RECEIPT_WINDOW_MS, 500);
|
|
1196
|
+
if (late)
|
|
1197
|
+
return "delivered";
|
|
1198
|
+
// Queued, and still no receipt after the full window. Do NOT fall through to the
|
|
1199
|
+
// retype: the text is demonstrably out of the composer, so retyping cannot undo
|
|
1200
|
+
// anything and can only stack a duplicate.
|
|
1201
|
+
//
|
|
1202
|
+
// "unconfirmed", never "undelivered". They are not interchangeable here:
|
|
1203
|
+
// `undelivered` makes the caller emit buildSubmitFailedResult(), whose text is
|
|
1204
|
+
// "Nothing was sent, so please send it again" -- a lie about a message sitting in
|
|
1205
|
+
// claude's queue, and an instruction that produces the exact duplicate this branch
|
|
1206
|
+
// exists to prevent. `unconfirmed` means "accepted, start not observed", which is
|
|
1207
|
+
// true, and the stall backstop is already the right net for it.
|
|
1208
|
+
return "unconfirmed";
|
|
1209
|
+
}
|
|
1210
|
+
this.logger.warn({ sessionId: this.sessionId, attempt, nextRetryMs: backoff }, "PtyQuery: no delivery receipt for the prompt; retrying");
|
|
1211
|
+
this.recordLine(`--- no delivery receipt (attempt ${attempt}); retrying in ${Math.round(backoff / 1000)}s ---`);
|
|
1212
|
+
await delay(backoff);
|
|
1213
|
+
backoff = Math.min(backoff * 2, DEAF_RETRY_MAX_MS);
|
|
1214
|
+
}
|
|
1215
|
+
return "undelivered";
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Press Enter to submit the composed prompt, and CONFIRM the turn actually started by
|
|
1219
|
+
* watching for the USER LINE claude appends to <sid>.jsonl the instant it accepts a turn.
|
|
1220
|
+
* During boot the TUI intermittently swallows the first Enter (a "what's new" notice /
|
|
1221
|
+
* MCP-auth warning steals focus), so retry until it takes.
|
|
1222
|
+
*
|
|
1223
|
+
* The confirmation used to be `transcriptSize() > before`, which is any growth at all.
|
|
1224
|
+
* That is not specific to the turn starting: claude writes `attachment`, `last-prompt`
|
|
1225
|
+
* and other bookkeeping lines on its own schedule, so a swallowed Enter that happened to
|
|
1226
|
+
* coincide with unrelated writes read as success. Observed in production 2026-08-01: a
|
|
1227
|
+
* session logged 157 consecutive `attachment` lines and NOT ONE `user` line, yet
|
|
1228
|
+
* submitTurn returned on the first attempt. The prompt stayed unsent in the TUI composer,
|
|
1229
|
+
* paseo set turnInFlight and waited on a turn that had never begun, and the agent showed
|
|
1230
|
+
* a spinner until the liveness backstop fired ten minutes later. The retry loop below was
|
|
1231
|
+
* always right; the thing it was checking was not.
|
|
1232
|
+
*/
|
|
1233
|
+
/**
|
|
1234
|
+
* Type the prompt and confirm the terminal actually received it, retrying against a
|
|
1235
|
+
* deaf terminal with backoff.
|
|
1236
|
+
*
|
|
1237
|
+
* Returns true once an echo (or a transcript user line) confirms the keystrokes landed;
|
|
1238
|
+
* false only after the terminal stayed deaf for the whole {@link READY_BUSY_MAX_WAIT_MS}
|
|
1239
|
+
* ceiling. Retyping cannot double-deliver: a deaf terminal demonstrably discards input
|
|
1240
|
+
* (zero echo), and every iteration first checks whether a user line landed anyway.
|
|
1241
|
+
*/
|
|
1242
|
+
async submitTurn(text, watermark, fingerprint) {
|
|
1243
|
+
const before = watermark;
|
|
1244
|
+
// A slash command gets EXACTLY ONE Enter. Never retry it.
|
|
1245
|
+
//
|
|
1246
|
+
// Typing "/x" opens the TUI's own command menu, and in that state each Enter runs the
|
|
1247
|
+
// highlighted entry. The retry loop below was added to beat a swallowed first Enter on
|
|
1248
|
+
// a booting terminal, and for ordinary prompts it works; for a slash command it
|
|
1249
|
+
// re-executes the command once per attempt. Captured on a recording:
|
|
1250
|
+
//
|
|
1251
|
+
// /compact <- written once by paseo
|
|
1252
|
+
// /compact Free up context by summarizing… <- the TUI's menu opens
|
|
1253
|
+
// > /compact > /compact Not enough messages to compact.
|
|
1254
|
+
//
|
|
1255
|
+
// Two executions from one message. That is why sessions compacted twice, and why a
|
|
1256
|
+
// second /compact could land on an already-compacted session at a few thousand tokens.
|
|
1257
|
+
// Confirmation cannot come from the transcript here either: a slash command does not
|
|
1258
|
+
// append the user line that sawUserLineSince() waits for, so the loop never sees
|
|
1259
|
+
// success and always burns all five attempts.
|
|
1260
|
+
//
|
|
1261
|
+
// Instead, confirm from the terminal: the command was accepted if the TUI produced
|
|
1262
|
+
// output in response. If it produced nothing, report it rather than pressing again --
|
|
1263
|
+
// a lost slash command is recoverable by resending, a duplicated one is not.
|
|
1264
|
+
if (text.trim().startsWith("/")) {
|
|
1265
|
+
const bytesBefore = this.ptyBytesTotal;
|
|
1266
|
+
await this.transport.writeRaw?.("\r");
|
|
1267
|
+
const reacted = await pollUntil(() => this.receiptSince(before, fingerprint) || this.ptyBytesTotal > bytesBefore, SLASH_COMMAND_ECHO_WAIT_MS, 150);
|
|
1268
|
+
// The terminal responding is not the same as the command running. An unknown command
|
|
1269
|
+
// is answered with `Unknown command: /X. Did you mean /y?` and NOTHING starts -- but
|
|
1270
|
+
// that reply previously counted as acceptance, so the app showed a running turn that
|
|
1271
|
+
// did not exist and the sender saw dead silence. Observed live: a phone-keyboard
|
|
1272
|
+
// auto-capitalized "/WhatsApp-read" rejected twice while the UI said "working".
|
|
1273
|
+
// Checked regardless of the byte-poll outcome: the rejection can repaint before the
|
|
1274
|
+
// poll's baseline is captured, in which case `reacted` is false but the refusal is
|
|
1275
|
+
// already sitting in the tail.
|
|
1276
|
+
await delay(400);
|
|
1277
|
+
const m = this.terminalTail.slice(-500).match(/Unknown command:[^\n]{0,160}/i);
|
|
1278
|
+
if (m) {
|
|
1279
|
+
this.rejectedCommandNotice = m[0].trim();
|
|
1280
|
+
this.recordLine(`--- CLI rejected the command: ${this.rejectedCommandNotice} ---`);
|
|
1281
|
+
return "rejected";
|
|
1282
|
+
}
|
|
1283
|
+
if (!reacted) {
|
|
1284
|
+
this.logger.warn({ sessionId: this.sessionId, command: text.trim().split(/\s/, 1)[0] }, "PtyQuery: slash command produced no terminal response; not retrying to avoid double execution");
|
|
1285
|
+
}
|
|
1286
|
+
// A slash command may not append a user record at all (version-dependent), so the
|
|
1287
|
+
// CLI's own reaction stands in for the receipt here. This reads a CLI RESPONSE, not
|
|
1288
|
+
// a rendering, and it is never retried: a lost slash command is recoverable by
|
|
1289
|
+
// resending, a duplicated one is not.
|
|
1290
|
+
return reacted ? "delivered" : "unconfirmed";
|
|
1291
|
+
}
|
|
1292
|
+
await this.transport.writeRaw?.("\r");
|
|
1293
|
+
if (await pollUntil(() => this.receiptSince(before, fingerprint), RECEIPT_WINDOW_MS, 150)) {
|
|
1294
|
+
return "delivered";
|
|
1295
|
+
}
|
|
1296
|
+
// No receipt from this attempt. The delivery loop decides what that means: it can see
|
|
1297
|
+
// whether the transcript is moving (queued behind a running turn) and owns the retry.
|
|
1298
|
+
this.logger.debug({ sessionId: this.sessionId, lastLine: this.lastMeaningfulLine.slice(-120) }, "PtyQuery: no receipt for this submit attempt");
|
|
1299
|
+
return "unconfirmed";
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* True once a real `user` line has been appended past `offset`. Reads only the bytes
|
|
1303
|
+
* added since then, so it stays cheap on a transcript that can reach tens of MB.
|
|
1304
|
+
* `isMeta` user lines are claude's own bookkeeping and do not mean a turn started.
|
|
1305
|
+
*/
|
|
1306
|
+
/**
|
|
1307
|
+
* Fingerprint used to match a transcript record against what we typed. Whitespace is
|
|
1308
|
+
* stripped on both sides because the terminal is free to re-wrap and re-space anything
|
|
1309
|
+
* it renders, while the transcript keeps the text verbatim - so the character sequence
|
|
1310
|
+
* is the part that survives every rendering.
|
|
1311
|
+
*/
|
|
1312
|
+
static fingerprint(text) {
|
|
1313
|
+
return text.replace(/\s+/g, "").slice(0, RECEIPT_FINGERPRINT_CHARS);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* THE delivery authority: did claude append a user record matching what we sent?
|
|
1317
|
+
*
|
|
1318
|
+
* Content-matched on purpose. The old check accepted ANY user record past the watermark,
|
|
1319
|
+
* which cannot tell "my message became a turn" apart from an unrelated record (a resume
|
|
1320
|
+
* dialog answering itself, a queued earlier message). Matching the text answers the only
|
|
1321
|
+
* question that matters, and answers it identically whether the terminal echoed the
|
|
1322
|
+
* characters, collapsed them into a paste chip, or drew nothing at all.
|
|
1323
|
+
*/
|
|
1324
|
+
receiptSince(offset, fingerprint) {
|
|
1325
|
+
for (const record of this.readUserRecordsSince(offset)) {
|
|
1326
|
+
const body = PtyQuery.fingerprint(record);
|
|
1327
|
+
if (!fingerprint)
|
|
1328
|
+
return true;
|
|
1329
|
+
// Short messages must match whole, or a two-letter fingerprint like "ok" would
|
|
1330
|
+
// happily match an unrelated record that merely contains those letters.
|
|
1331
|
+
if (fingerprint.length < RECEIPT_EXACT_MATCH_BELOW_CHARS) {
|
|
1332
|
+
if (body === fingerprint)
|
|
1333
|
+
return true;
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (body.includes(fingerprint))
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
/** Text of every non-meta user record written past `offset`. */
|
|
1342
|
+
readUserRecordsSince(offset) {
|
|
1343
|
+
let fd = null;
|
|
1344
|
+
const out = [];
|
|
1345
|
+
try {
|
|
1346
|
+
const size = this.transcriptSize();
|
|
1347
|
+
if (size <= offset)
|
|
1348
|
+
return out;
|
|
1349
|
+
fd = fs.openSync(this.transcriptPath, "r");
|
|
1350
|
+
const length = size - offset;
|
|
1351
|
+
const buffer = Buffer.alloc(length);
|
|
1352
|
+
fs.readSync(fd, buffer, 0, length, offset);
|
|
1353
|
+
for (const line of buffer.toString("utf8").split("\n")) {
|
|
1354
|
+
if (!line.trim())
|
|
1355
|
+
continue;
|
|
1356
|
+
try {
|
|
1357
|
+
// A trailing partial line simply fails to parse; the next poll sees it whole.
|
|
1358
|
+
const parsed = JSON.parse(line);
|
|
1359
|
+
if (parsed?.type !== "user" || parsed.isMeta)
|
|
1360
|
+
continue;
|
|
1361
|
+
out.push(userRecordText(parsed.message?.content));
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
/* partial or non-JSON line */
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return out;
|
|
1368
|
+
}
|
|
1369
|
+
catch {
|
|
1370
|
+
return out;
|
|
1371
|
+
}
|
|
1372
|
+
finally {
|
|
1373
|
+
if (fd !== null) {
|
|
1374
|
+
try {
|
|
1375
|
+
fs.closeSync(fd);
|
|
1376
|
+
}
|
|
1377
|
+
catch {
|
|
1378
|
+
/* ignore */
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
transcriptSize() {
|
|
1384
|
+
try {
|
|
1385
|
+
return fs.statSync(this.transcriptPath).size;
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
return 0;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// ---- transcript → SDKMessage stream + turn-end ----------------------------
|
|
1392
|
+
onTranscriptMessage(message) {
|
|
1393
|
+
this.emit(message);
|
|
1394
|
+
this.trackInteractiveDialog(message);
|
|
1395
|
+
if (message.type === "assistant") {
|
|
1396
|
+
const beta = message.message;
|
|
1397
|
+
this.lastStopReason = beta.stop_reason ?? null;
|
|
1398
|
+
if (beta.usage)
|
|
1399
|
+
this.lastUsage = beta.usage;
|
|
1400
|
+
}
|
|
1401
|
+
// Any transcript activity proves the turn is alive; push the stall deadline out.
|
|
1402
|
+
this.lastTranscriptAt = monotonicNowMs();
|
|
1403
|
+
// Real turn activity means the modal is gone, whatever the tail still remembers.
|
|
1404
|
+
this.interruptPromptAt = 0;
|
|
1405
|
+
this.armStallTimer();
|
|
1406
|
+
this.scheduleTurnEndCheck();
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Track interactive TUI dialogs (AskUserQuestion / plan-mode approval) from the
|
|
1410
|
+
* transcript. While one is pending the terminal is WAITING, not stalled: the stall
|
|
1411
|
+
* backstop must stand down, no ESC may be sent (it declines the dialog - the source of
|
|
1412
|
+
* every phantom "User declined" this week), and an incoming reply is translated to
|
|
1413
|
+
* dialog keystrokes instead of typed text.
|
|
1414
|
+
*/
|
|
1415
|
+
trackInteractiveDialog(message) {
|
|
1416
|
+
const content = message.message?.content;
|
|
1417
|
+
if (!Array.isArray(content))
|
|
1418
|
+
return;
|
|
1419
|
+
for (const block of content) {
|
|
1420
|
+
if (!block || typeof block !== "object")
|
|
1421
|
+
continue;
|
|
1422
|
+
const b = block;
|
|
1423
|
+
if (message.type === "assistant" &&
|
|
1424
|
+
b.type === "tool_use" &&
|
|
1425
|
+
b.id &&
|
|
1426
|
+
(b.name === "AskUserQuestion" || b.name === "ExitPlanMode")) {
|
|
1427
|
+
this.pendingDialog = {
|
|
1428
|
+
toolUseId: b.id,
|
|
1429
|
+
name: b.name,
|
|
1430
|
+
input: isRecord(b.input) ? b.input : {},
|
|
1431
|
+
options: extractDialogOptions(b.name, b.input),
|
|
1432
|
+
optionsByQuestion: extractDialogOptionsByQuestion(b.name, b.input),
|
|
1433
|
+
};
|
|
1434
|
+
this.recordLine(`--- interactive dialog pending: ${b.name} ---`);
|
|
1435
|
+
this.notifyDialogChange();
|
|
1436
|
+
}
|
|
1437
|
+
const pending = this.pendingDialog;
|
|
1438
|
+
if (pending && b.type === "tool_result" && b.tool_use_id === pending.toolUseId) {
|
|
1439
|
+
this.recordLine(`--- interactive dialog answered: ${pending.name} ---`);
|
|
1440
|
+
this.pendingDialog = null;
|
|
1441
|
+
this.notifyDialogChange();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
scheduleTurnEndCheck() {
|
|
1446
|
+
if (this.turnEndTimer)
|
|
1447
|
+
clearTimeout(this.turnEndTimer);
|
|
1448
|
+
this.turnEndTimer = setTimeout(() => this.maybeCompleteTurn(), TURN_END_IDLE_MS);
|
|
1449
|
+
}
|
|
1450
|
+
maybeCompleteTurn() {
|
|
1451
|
+
if (this.done)
|
|
1452
|
+
return;
|
|
1453
|
+
const stop = this.lastStopReason;
|
|
1454
|
+
if (!stop || !TERMINAL_STOP_REASONS.has(stop))
|
|
1455
|
+
return; // e.g. "tool_use" → more coming
|
|
1456
|
+
this.numTurns += 1;
|
|
1457
|
+
// The turn ended on its own; stand the liveness backstop down before emitting.
|
|
1458
|
+
this.turnInFlight = false;
|
|
1459
|
+
this.clearStallTimer();
|
|
1460
|
+
this.stopHeartbeat();
|
|
1461
|
+
this.emit(this.buildResult(stop));
|
|
1462
|
+
// reset so a stale terminal reason can't re-fire before the next assistant turn
|
|
1463
|
+
this.lastStopReason = null;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* (Re)arm the liveness backstop. Called when a turn starts and on every transcript
|
|
1467
|
+
* message, so the deadline only expires after an unbroken window of silence.
|
|
1468
|
+
*/
|
|
1469
|
+
armStallTimer() {
|
|
1470
|
+
if (this.stallTimeoutMs <= 0 || !this.turnInFlight)
|
|
1471
|
+
return;
|
|
1472
|
+
this.clearStallTimer();
|
|
1473
|
+
this.stallTimer = setTimeout(() => this.onTurnStalled(), this.stallTimeoutMs);
|
|
1474
|
+
// Never hold the event loop open just to wait for a stall.
|
|
1475
|
+
this.stallTimer.unref?.();
|
|
1476
|
+
}
|
|
1477
|
+
clearStallTimer() {
|
|
1478
|
+
if (this.stallTimer)
|
|
1479
|
+
clearTimeout(this.stallTimer);
|
|
1480
|
+
this.stallTimer = null;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* No transcript activity for the whole budget while a turn was outstanding. Surface it as
|
|
1484
|
+
* a failed turn instead of leaving the caller on an endless spinner. We do NOT kill the
|
|
1485
|
+
* PTY: claude may still be retrying, and if it recovers its messages keep flowing through
|
|
1486
|
+
* the reader as a fresh turn.
|
|
1487
|
+
*/
|
|
1488
|
+
onTurnStalled() {
|
|
1489
|
+
this.stallTimer = null;
|
|
1490
|
+
if (this.done || !this.turnInFlight)
|
|
1491
|
+
return;
|
|
1492
|
+
if (this.pendingDialog) {
|
|
1493
|
+
// Waiting on a human is not a stall. Observed live 2026-08-04: an ExitPlanMode
|
|
1494
|
+
// approval sat pending for 2.5 hours and the backstop declared the turn failed,
|
|
1495
|
+
// which read as a wedge and got a healthy process killed.
|
|
1496
|
+
this.recordLine(`--- stall budget elapsed but ${this.pendingDialog.name} is pending ---`);
|
|
1497
|
+
this.armStallTimer();
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
this.turnInFlight = false;
|
|
1501
|
+
this.stopHeartbeat();
|
|
1502
|
+
const silentForMs = this.stallTimeoutMs;
|
|
1503
|
+
const terminalTail = this.readTerminalTail();
|
|
1504
|
+
this.recordLine(`--- STALLED after ${silentForMs}ms of transcript silence ---`);
|
|
1505
|
+
this.logger.error({
|
|
1506
|
+
sessionId: this.sessionId,
|
|
1507
|
+
transcriptPath: this.transcriptPath,
|
|
1508
|
+
silentForMs,
|
|
1509
|
+
// The whole point of the tap: whatever the TUI was showing while it went quiet.
|
|
1510
|
+
terminalTail,
|
|
1511
|
+
}, "PtyQuery: turn stalled, no transcript activity within the liveness budget");
|
|
1512
|
+
this.numTurns += 1;
|
|
1513
|
+
this.emit(this.buildStalledResult(silentForMs, terminalTail));
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Append terminal output to the rolling tail.
|
|
1517
|
+
*
|
|
1518
|
+
* Scrubbed and filtered on the way IN, not on the way out, because the TUI's spinner
|
|
1519
|
+
* redraws at a few hundred bytes/second: a raw buffer holds only a second or two of
|
|
1520
|
+
* history and would evict the one line worth reading. Keeping only lines that carry
|
|
1521
|
+
* letters or digits means a message like "Compacting conversation…" survives minutes of
|
|
1522
|
+
* animation. Verified against a real stall: the raw tail was 163 chars of pure spinner.
|
|
1523
|
+
*/
|
|
1524
|
+
/**
|
|
1525
|
+
* True when the rolling tail's last frame looks like an input box waiting for typing.
|
|
1526
|
+
*
|
|
1527
|
+
* Deliberately requires the busy marker to be ABSENT rather than trusting the prompt
|
|
1528
|
+
* markers alone: the working footer renders as `… · esc to interrupt · ← for agents`, so
|
|
1529
|
+
* "for agents" on its own is present in both states and would read a mid-turn terminal
|
|
1530
|
+
* as idle.
|
|
1531
|
+
*/
|
|
1532
|
+
/** Runs `fn` after every previously queued terminal write has finished. */
|
|
1533
|
+
runExclusive(fn) {
|
|
1534
|
+
const next = this.writeGate.then(fn, fn);
|
|
1535
|
+
// Keep the chain alive even when a link rejects; each caller still sees its own error.
|
|
1536
|
+
this.writeGate = next.catch(() => undefined);
|
|
1537
|
+
return next;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Type a control slash command (`/model`, `/effort`) into the terminal, but only once the
|
|
1541
|
+
* TUI is actually accepting input, and never while another write is in flight.
|
|
1542
|
+
*
|
|
1543
|
+
* Why this exists instead of `transport.write()` straight from the agent: a control command
|
|
1544
|
+
* written to the transport bypasses the input loop, so it races the prompt being delivered
|
|
1545
|
+
* and the two COLLIDE on the same input line. Captured live 2026-08-29 (session c132c833)
|
|
1546
|
+
* from claude's own transcript:
|
|
1547
|
+
*
|
|
1548
|
+
* command-args>claude-opus-5[1m]Responda APENAS: R1<
|
|
1549
|
+
*
|
|
1550
|
+
* The model id and the user's prompt, concatenated with no separator. Measured
|
|
1551
|
+
* consequences: the model never changed (the old one kept answering two minutes later),
|
|
1552
|
+
* the user's prompt was swallowed by the slash command, and the agent never returned to
|
|
1553
|
+
* idle. Waiting for the input box also keeps the command out of claude's PERSISTED input
|
|
1554
|
+
* queue, so it cannot be replayed into a future session.
|
|
1555
|
+
*
|
|
1556
|
+
* Returns false when the terminal never became typeable, so the caller can refuse to
|
|
1557
|
+
* record a setting change that did not happen.
|
|
1558
|
+
*/
|
|
1559
|
+
async sendControlCommand(command) {
|
|
1560
|
+
return await this.runExclusive(async () => {
|
|
1561
|
+
const deadline = monotonicNowMs() + CONTROL_COMMAND_MAX_WAIT_MS;
|
|
1562
|
+
while (!this.done && monotonicNowMs() < deadline) {
|
|
1563
|
+
if (this.looksIdle()) {
|
|
1564
|
+
// Text first, Enter SEPARATELY -- never `write(`${command}\r`)` as one chunk.
|
|
1565
|
+
//
|
|
1566
|
+
// Typing "/" opens the TUI's own command menu, and a carriage return arriving in the
|
|
1567
|
+
// same chunk as the text is taken as a newline inside the composer rather than a
|
|
1568
|
+
// submit. The command then just sits there, and the next thing typed is appended to
|
|
1569
|
+
// it. Captured live 2026-08-29 14:00:47 on a daemon that already had the write gate:
|
|
1570
|
+
// paseo wrote a clean `/effort low\r` (provenance `length: 12`) and the CLI still
|
|
1571
|
+
// received
|
|
1572
|
+
//
|
|
1573
|
+
// command-args>low\nThis is me changing effort to low<
|
|
1574
|
+
//
|
|
1575
|
+
// the level and the user's next message as one argument. submitTurn() has always
|
|
1576
|
+
// sent Enter as its own writeRaw for exactly this reason; this path did not.
|
|
1577
|
+
const bytesBefore = this.ptyBytesTotal;
|
|
1578
|
+
await this.transport.write(command);
|
|
1579
|
+
await this.transport.writeRaw?.("\r");
|
|
1580
|
+
// Exactly one Enter, never retried: a lost control command is recoverable by
|
|
1581
|
+
// resending, a duplicated one runs the command twice (see submitTurn).
|
|
1582
|
+
const reacted = await pollUntil(() => this.ptyBytesTotal > bytesBefore, SLASH_COMMAND_ECHO_WAIT_MS, 150);
|
|
1583
|
+
if (!reacted) {
|
|
1584
|
+
this.recordLine(`--- "${command}" produced no terminal response ---`);
|
|
1585
|
+
this.logger.warn({ sessionId: this.sessionId, command }, "PtyQuery: control command produced no terminal response; not retrying");
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
await delay(CONTROL_COMMAND_POLL_MS);
|
|
1591
|
+
}
|
|
1592
|
+
if (this.done)
|
|
1593
|
+
return false;
|
|
1594
|
+
this.recordLine(`--- terminal never accepted input; "${command}" not sent ---`);
|
|
1595
|
+
this.logger.warn({ sessionId: this.sessionId, command, waitedMs: CONTROL_COMMAND_MAX_WAIT_MS }, "PtyQuery: terminal never returned to an input box, refusing to type a control command");
|
|
1596
|
+
return false;
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
looksIdle() {
|
|
1600
|
+
const tail = this.terminalTail.slice(-400).toLowerCase();
|
|
1601
|
+
if (!tail)
|
|
1602
|
+
return false;
|
|
1603
|
+
if (tail.includes(BUSY_MARKER))
|
|
1604
|
+
return false;
|
|
1605
|
+
if (this.lastBusyAt > 0 && monotonicNowMs() - this.lastBusyAt < READY_IDLE_STABLE_MS)
|
|
1606
|
+
return false;
|
|
1607
|
+
return IDLE_PROMPT_MARKERS.some((m) => tail.includes(m));
|
|
1608
|
+
}
|
|
1609
|
+
appendTerminalTail(chunk) {
|
|
1610
|
+
if (!chunk)
|
|
1611
|
+
return;
|
|
1612
|
+
this.ptyBytesTotal += chunk.length;
|
|
1613
|
+
const text = stripTerminalControl(chunk);
|
|
1614
|
+
if (!text)
|
|
1615
|
+
return;
|
|
1616
|
+
if (this.echoProbe) {
|
|
1617
|
+
this.echoProbe.buf = (this.echoProbe.buf + text.replace(/\s+/g, "")).slice(-2000);
|
|
1618
|
+
}
|
|
1619
|
+
// Read the busy marker off the raw stripped text, NOT the filtered tail below:
|
|
1620
|
+
// isProgressNoise() drops a line that is only the footer, which is exactly the frame
|
|
1621
|
+
// that proves a turn is running.
|
|
1622
|
+
if (text.toLowerCase().includes(BUSY_MARKER))
|
|
1623
|
+
this.lastBusyAt = monotonicNowMs();
|
|
1624
|
+
for (const rawLine of text.split("\n")) {
|
|
1625
|
+
const line = rawLine.trim();
|
|
1626
|
+
if (!line || !/[a-z0-9]/i.test(line))
|
|
1627
|
+
continue;
|
|
1628
|
+
if (isProgressNoise(line))
|
|
1629
|
+
continue;
|
|
1630
|
+
// de-dupe consecutive identical redraws of the same line
|
|
1631
|
+
if (this.terminalTail.endsWith(line + "\n"))
|
|
1632
|
+
continue;
|
|
1633
|
+
this.terminalTail += line + "\n";
|
|
1634
|
+
this.lastMeaningfulLine = line;
|
|
1635
|
+
if (line.toLowerCase().includes(INTERRUPT_PROMPT_MARKER)) {
|
|
1636
|
+
this.interruptPromptAt = monotonicNowMs();
|
|
1637
|
+
}
|
|
1638
|
+
if (line.toLowerCase().includes(RESUME_DIALOG_MARKER)) {
|
|
1639
|
+
this.resumeDialogAt = monotonicNowMs();
|
|
1640
|
+
}
|
|
1641
|
+
this.recordLine(line);
|
|
1642
|
+
}
|
|
1643
|
+
if (this.terminalTail.length > TERMINAL_TAIL_MAX_CHARS) {
|
|
1644
|
+
this.terminalTail = this.terminalTail.slice(-TERMINAL_TAIL_MAX_CHARS);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// ---- telemetry ------------------------------------------------------------
|
|
1648
|
+
/** Open `$PASEO_HOME/pty-recordings/<sessionId>.log`. Best-effort: never fail a session. */
|
|
1649
|
+
openRecording() {
|
|
1650
|
+
try {
|
|
1651
|
+
const home = process.env.PASEO_HOME;
|
|
1652
|
+
if (!home)
|
|
1653
|
+
return;
|
|
1654
|
+
const dir = path.join(home, "pty-recordings");
|
|
1655
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1656
|
+
this.recordingPath = path.join(dir, `${this.sessionId}.log`);
|
|
1657
|
+
this.recordingBytes = this.safeSize(this.recordingPath);
|
|
1658
|
+
this.recordLine(`--- PtyQuery attached, transcript=${this.transcriptPath} ---`);
|
|
1659
|
+
this.logger.debug({ sessionId: this.sessionId, recordingPath: this.recordingPath }, "PtyQuery: recording terminal output");
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
this.recordingPath = null;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
recordLine(line) {
|
|
1666
|
+
if (!this.recordingPath)
|
|
1667
|
+
return;
|
|
1668
|
+
try {
|
|
1669
|
+
const entry = `${new Date().toISOString()} ${line}\n`;
|
|
1670
|
+
fs.appendFileSync(this.recordingPath, entry);
|
|
1671
|
+
this.recordingBytes += entry.length;
|
|
1672
|
+
if (this.recordingBytes > RECORDING_MAX_BYTES) {
|
|
1673
|
+
// Keep the recent past, drop the rest. Diagnostics, not an audit trail.
|
|
1674
|
+
const kept = fs.readFileSync(this.recordingPath).subarray(-RECORDING_KEEP_BYTES);
|
|
1675
|
+
fs.writeFileSync(this.recordingPath, kept);
|
|
1676
|
+
this.recordingBytes = kept.length;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
catch {
|
|
1680
|
+
this.recordingPath = null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
safeSize(p) {
|
|
1684
|
+
try {
|
|
1685
|
+
return fs.statSync(p).size;
|
|
1686
|
+
}
|
|
1687
|
+
catch {
|
|
1688
|
+
return 0;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* While a turn is outstanding, emit one line every {@link LIVENESS_HEARTBEAT_MS} with
|
|
1693
|
+
* enough state to tell the failure modes apart without waiting for the stall budget:
|
|
1694
|
+
* terminal bytes since the last beat (0 means the TUI is frozen, not thinking), seconds
|
|
1695
|
+
* since the transcript last grew, and the last meaningful line on screen.
|
|
1696
|
+
*/
|
|
1697
|
+
startHeartbeat() {
|
|
1698
|
+
this.stopHeartbeat();
|
|
1699
|
+
this.ptyBytesAtLastBeat = this.ptyBytesTotal;
|
|
1700
|
+
this.heartbeatTimer = setInterval(() => this.beat(), LIVENESS_HEARTBEAT_MS);
|
|
1701
|
+
this.heartbeatTimer.unref?.();
|
|
1702
|
+
}
|
|
1703
|
+
stopHeartbeat() {
|
|
1704
|
+
if (this.heartbeatTimer)
|
|
1705
|
+
clearInterval(this.heartbeatTimer);
|
|
1706
|
+
this.heartbeatTimer = null;
|
|
1707
|
+
}
|
|
1708
|
+
beat() {
|
|
1709
|
+
if (this.done || !this.turnInFlight) {
|
|
1710
|
+
this.stopHeartbeat();
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const ptyBytesSinceLastBeat = this.ptyBytesTotal - this.ptyBytesAtLastBeat;
|
|
1714
|
+
this.ptyBytesAtLastBeat = this.ptyBytesTotal;
|
|
1715
|
+
this.logger.info({
|
|
1716
|
+
sessionId: this.sessionId,
|
|
1717
|
+
event: "pty.turn.heartbeat",
|
|
1718
|
+
turnAgeMs: this.turnStartedAt ? monotonicNowMs() - this.turnStartedAt : 0,
|
|
1719
|
+
ptyBytesSinceLastBeat,
|
|
1720
|
+
secondsSinceTranscript: this.lastTranscriptAt
|
|
1721
|
+
? Math.round((monotonicNowMs() - this.lastTranscriptAt) / 1000)
|
|
1722
|
+
: null,
|
|
1723
|
+
readySettledBy: this.readySettledBy,
|
|
1724
|
+
lastLine: this.lastMeaningfulLine.slice(-160),
|
|
1725
|
+
}, "PtyQuery: turn in flight");
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* The tail as readable text: ANSI/OSC sequences dropped and runs of whitespace collapsed,
|
|
1729
|
+
* because a raw TUI capture is mostly cursor movement and redraw padding.
|
|
1730
|
+
*/
|
|
1731
|
+
readTerminalTail() {
|
|
1732
|
+
return this.terminalTail.trim().slice(-TERMINAL_TAIL_MAX_CHARS);
|
|
1733
|
+
}
|
|
1734
|
+
buildResult(stopReason) {
|
|
1735
|
+
const usage = (this.lastUsage ?? {
|
|
1736
|
+
input_tokens: 0,
|
|
1737
|
+
output_tokens: 0,
|
|
1738
|
+
cache_creation_input_tokens: 0,
|
|
1739
|
+
cache_read_input_tokens: 0,
|
|
1740
|
+
});
|
|
1741
|
+
const result = {
|
|
1742
|
+
type: "result",
|
|
1743
|
+
subtype: "success",
|
|
1744
|
+
duration_ms: this.turnStartedAt ? Math.round(monotonicNowMs() - this.turnStartedAt) : 0,
|
|
1745
|
+
duration_api_ms: 0,
|
|
1746
|
+
is_error: false,
|
|
1747
|
+
num_turns: this.numTurns,
|
|
1748
|
+
result: "",
|
|
1749
|
+
stop_reason: stopReason,
|
|
1750
|
+
total_cost_usd: 0,
|
|
1751
|
+
usage,
|
|
1752
|
+
modelUsage: {},
|
|
1753
|
+
permission_denials: [],
|
|
1754
|
+
uuid: randomUUID(),
|
|
1755
|
+
session_id: this.sessionId,
|
|
1756
|
+
};
|
|
1757
|
+
return result;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* The agent process exited. Fail any in-flight turn immediately and end the query.
|
|
1761
|
+
*
|
|
1762
|
+
* Unlike a stall there is nothing to wait for: no retry, no recovery, no later transcript
|
|
1763
|
+
* activity. Emitting here rather than letting the stall timer fire turns a ten-minute wait
|
|
1764
|
+
* plus a false "still running" into an immediate, accurate failure, and finishing the query
|
|
1765
|
+
* stops the layer above treating this transport as a live destination for prompts.
|
|
1766
|
+
*/
|
|
1767
|
+
onTransportExit(code, signal) {
|
|
1768
|
+
// Recorded before the `done` bail: the wording helpers and the delivery guards must know
|
|
1769
|
+
// the process is gone even when the query had already been torn down.
|
|
1770
|
+
this.transportExited ?? (this.transportExited = { code, signal });
|
|
1771
|
+
if (this.done)
|
|
1772
|
+
return;
|
|
1773
|
+
const wasInFlight = this.turnInFlight;
|
|
1774
|
+
this.turnInFlight = false;
|
|
1775
|
+
this.clearStallTimer();
|
|
1776
|
+
this.logger.error({ sessionId: this.sessionId, exitCode: code, exitSignal: signal, wasInFlight }, "PtyQuery: the agent process exited");
|
|
1777
|
+
this.recordLine(`--- agent process exited (status ${code ?? "none"}, signal ${signal ?? "none"}) ---`);
|
|
1778
|
+
if (wasInFlight) {
|
|
1779
|
+
this.numTurns += 1;
|
|
1780
|
+
this.emit(this.buildExitedResult(code, signal));
|
|
1781
|
+
}
|
|
1782
|
+
this.finish();
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* A non-success `result` for an agent that exited.
|
|
1786
|
+
*
|
|
1787
|
+
* Wording constraints match {@link buildStalledResult}: `isAbortError()` drops results
|
|
1788
|
+
* matching /\baborted\b/i and `buildTurnFailedEvent()` scrapes /\bcode\s+(\d+)\b/i for an
|
|
1789
|
+
* exit code, so this says "status", never "code", and never says "aborted". It also must
|
|
1790
|
+
* NOT claim the session is still running, which is the exact lie this change deletes.
|
|
1791
|
+
*/
|
|
1792
|
+
buildExitedResult(code, signal) {
|
|
1793
|
+
const base = this.buildResult("process_exited");
|
|
1794
|
+
const how = signal ? `on signal ${signal}` : `with status ${code ?? "unknown"}`;
|
|
1795
|
+
const excerpt = this.terminalTail.slice(-TERMINAL_TAIL_EXCERPT_CHARS).trim();
|
|
1796
|
+
const tailLine = excerpt ? ` Last terminal output: ${JSON.stringify(excerpt)}.` : "";
|
|
1797
|
+
return {
|
|
1798
|
+
...base,
|
|
1799
|
+
subtype: "error_during_execution",
|
|
1800
|
+
is_error: true,
|
|
1801
|
+
errors: [
|
|
1802
|
+
`The agent process exited ${how} before finishing this turn, so the session is NOT ` +
|
|
1803
|
+
`running any more and will not recover on its own. The conversation is still on ` +
|
|
1804
|
+
`disk: recover it with "paseo import ${this.sessionId}", then restore the agent's ` +
|
|
1805
|
+
`model, thinking budget and permission mode, which import does not carry over.` +
|
|
1806
|
+
tailLine,
|
|
1807
|
+
],
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
buildStalledResult(silentForMs, terminalTail) {
|
|
1811
|
+
// A dead process is not a stall, and must never be described as one that "may recover".
|
|
1812
|
+
if (this.transportExited) {
|
|
1813
|
+
return this.buildExitedResult(this.transportExited.code, this.transportExited.signal);
|
|
1814
|
+
}
|
|
1815
|
+
const minutes = Math.round(silentForMs / 60000);
|
|
1816
|
+
const base = this.buildResult("stall_timeout");
|
|
1817
|
+
// The last thing the terminal showed is usually the whole diagnosis ("Compacting
|
|
1818
|
+
// conversation…", a retry notice), so lead the message with it when we have one.
|
|
1819
|
+
const excerpt = (terminalTail ?? "").slice(-TERMINAL_TAIL_EXCERPT_CHARS).trim();
|
|
1820
|
+
const tailLine = excerpt ? ` Last terminal output: ${JSON.stringify(excerpt)}.` : "";
|
|
1821
|
+
return {
|
|
1822
|
+
...base,
|
|
1823
|
+
subtype: "error_during_execution",
|
|
1824
|
+
is_error: true,
|
|
1825
|
+
errors: [
|
|
1826
|
+
`No response from the claude PTY session for ${minutes} min. ` +
|
|
1827
|
+
`The turn was accepted but produced no output, so it was marked failed. ` +
|
|
1828
|
+
`The underlying session is still running and may recover.${tailLine}`,
|
|
1829
|
+
],
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
// ---- control surface ------------------------------------------------------
|
|
1833
|
+
async interrupt() {
|
|
1834
|
+
// ESC interrupts the active turn in the claude TUI.
|
|
1835
|
+
await this.transport.writeRaw?.("\x1b");
|
|
1836
|
+
}
|
|
1837
|
+
// These are driven by the `instanceof PtyTransport` branches in agent.ts (which write
|
|
1838
|
+
// to the transport directly), so on the query they are inert.
|
|
1839
|
+
async setPermissionMode() { }
|
|
1840
|
+
async setModel() { }
|
|
1841
|
+
async setMaxThinkingTokens() { }
|
|
1842
|
+
async applyFlagSettings() { }
|
|
1843
|
+
async supportedModels() {
|
|
1844
|
+
return [];
|
|
1845
|
+
}
|
|
1846
|
+
async supportedCommands() {
|
|
1847
|
+
return [];
|
|
1848
|
+
}
|
|
1849
|
+
async supportedAgents() {
|
|
1850
|
+
return [];
|
|
1851
|
+
}
|
|
1852
|
+
async mcpServerStatus() {
|
|
1853
|
+
return [];
|
|
1854
|
+
}
|
|
1855
|
+
close() {
|
|
1856
|
+
this.finish();
|
|
1857
|
+
void this.transport.kill().catch(() => { });
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
/** Extract the plain-text prompt from a user SDK message for typing into the PTY. */
|
|
1861
|
+
/** Delay between Down-arrow presses when answering a dialog; the TUI animates selection. */
|
|
1862
|
+
const DIALOG_KEY_DELAY_MS = 150;
|
|
1863
|
+
/** How long to wait for the dialog's tool_result to land after answering keystrokes. */
|
|
1864
|
+
const DIALOG_ANSWER_CONFIRM_MS = 8000;
|
|
1865
|
+
/**
|
|
1866
|
+
* Options the TUI dialog offers, in on-screen order. AskUserQuestion carries them in its
|
|
1867
|
+
* input; the plan-approval dialog's options are the CLI's own and version-dependent, so
|
|
1868
|
+
* they stay empty and replies are matched by number only.
|
|
1869
|
+
*/
|
|
1870
|
+
function extractDialogOptionsByQuestion(name, input) {
|
|
1871
|
+
if (name !== "AskUserQuestion")
|
|
1872
|
+
return [];
|
|
1873
|
+
const questions = input?.questions;
|
|
1874
|
+
if (!Array.isArray(questions))
|
|
1875
|
+
return [];
|
|
1876
|
+
return questions.map((question) => {
|
|
1877
|
+
const options = question?.options;
|
|
1878
|
+
if (!Array.isArray(options))
|
|
1879
|
+
return [];
|
|
1880
|
+
const labels = [];
|
|
1881
|
+
for (const option of options) {
|
|
1882
|
+
if (typeof option === "string")
|
|
1883
|
+
labels.push(option);
|
|
1884
|
+
else if (option && typeof option === "object") {
|
|
1885
|
+
const label = option.label;
|
|
1886
|
+
if (typeof label === "string")
|
|
1887
|
+
labels.push(label);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return labels;
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
function extractDialogOptions(name, input) {
|
|
1894
|
+
if (name !== "AskUserQuestion")
|
|
1895
|
+
return [];
|
|
1896
|
+
const questions = input?.questions;
|
|
1897
|
+
if (!Array.isArray(questions) || questions.length === 0)
|
|
1898
|
+
return [];
|
|
1899
|
+
const options = questions[0]?.options;
|
|
1900
|
+
if (!Array.isArray(options))
|
|
1901
|
+
return [];
|
|
1902
|
+
const labels = [];
|
|
1903
|
+
for (const option of options) {
|
|
1904
|
+
if (typeof option === "string")
|
|
1905
|
+
labels.push(option);
|
|
1906
|
+
else if (option && typeof option === "object") {
|
|
1907
|
+
const label = option.label;
|
|
1908
|
+
if (typeof label === "string")
|
|
1909
|
+
labels.push(label);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return labels;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Resolve the user's reply to a 0-based dialog option index, or null when it names none.
|
|
1916
|
+
* "2" picks the second option; an exact case-insensitive label match works too. Free text
|
|
1917
|
+
* stays null on purpose: guessing would answer a question the user did not answer.
|
|
1918
|
+
*/
|
|
1919
|
+
function normalizeOptionText(value) {
|
|
1920
|
+
return value
|
|
1921
|
+
.trim()
|
|
1922
|
+
.toLowerCase()
|
|
1923
|
+
.replace(/[\u2018\u2019\u201c\u201d]/g, "'")
|
|
1924
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
1925
|
+
.trim();
|
|
1926
|
+
}
|
|
1927
|
+
/** The single index every candidate list agrees on, or null when it is 0 or 2+. */
|
|
1928
|
+
function soleIndex(indices) {
|
|
1929
|
+
return indices.length === 1 ? (indices[0] ?? null) : null;
|
|
1930
|
+
}
|
|
1931
|
+
function matchDialogOption(text, options) {
|
|
1932
|
+
const reply = text.trim();
|
|
1933
|
+
if (/^[1-9]$/.test(reply)) {
|
|
1934
|
+
const index = Number(reply) - 1;
|
|
1935
|
+
if (options.length === 0 || index < options.length)
|
|
1936
|
+
return index;
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
const lowered = reply.toLowerCase();
|
|
1940
|
+
const byLabel = options.findIndex((o) => o.trim().toLowerCase() === lowered);
|
|
1941
|
+
if (byLabel >= 0)
|
|
1942
|
+
return byLabel;
|
|
1943
|
+
// Exact-label-only matching sounds safe and is not: the app's own suggestion chip sent
|
|
1944
|
+
// "Yes" against an option labelled "Yes, merge to master", scored no match, and the
|
|
1945
|
+
// message was discarded (2026-08-21, question unanswered for 1h38m). Widen to
|
|
1946
|
+
// punctuation-insensitive equality, then to a UNIQUE prefix, then to a UNIQUE substring.
|
|
1947
|
+
// Uniqueness is the whole safety property: anything matching two options stays null,
|
|
1948
|
+
// because guessing answers a question the user did not answer.
|
|
1949
|
+
const normalizedReply = normalizeOptionText(reply);
|
|
1950
|
+
if (normalizedReply.length === 0)
|
|
1951
|
+
return null;
|
|
1952
|
+
const normalized = options.map(normalizeOptionText);
|
|
1953
|
+
const exact = normalized.flatMap((o, i) => (o === normalizedReply ? [i] : []));
|
|
1954
|
+
const byExact = soleIndex(exact);
|
|
1955
|
+
if (byExact !== null)
|
|
1956
|
+
return byExact;
|
|
1957
|
+
const prefix = normalized.flatMap((o, i) => (o.startsWith(normalizedReply) ? [i] : []));
|
|
1958
|
+
const byPrefix = soleIndex(prefix);
|
|
1959
|
+
if (byPrefix !== null)
|
|
1960
|
+
return byPrefix;
|
|
1961
|
+
// Only worth a substring pass when the reply is long enough to be a real quotation of
|
|
1962
|
+
// the label; a two-character reply substring-matching one option is a coincidence.
|
|
1963
|
+
if (normalizedReply.length < 4)
|
|
1964
|
+
return null;
|
|
1965
|
+
const contains = normalized.flatMap((o, i) => (o.includes(normalizedReply) ? [i] : []));
|
|
1966
|
+
return soleIndex(contains);
|
|
1967
|
+
}
|
|
1968
|
+
/** Flatten a transcript user record's content to plain text for fingerprint matching. */
|
|
1969
|
+
function userRecordText(content) {
|
|
1970
|
+
if (typeof content === "string")
|
|
1971
|
+
return content;
|
|
1972
|
+
if (!Array.isArray(content))
|
|
1973
|
+
return "";
|
|
1974
|
+
const parts = [];
|
|
1975
|
+
for (const block of content) {
|
|
1976
|
+
if (typeof block === "string")
|
|
1977
|
+
parts.push(block);
|
|
1978
|
+
else if (isRecord(block) && typeof block.text === "string")
|
|
1979
|
+
parts.push(block.text);
|
|
1980
|
+
}
|
|
1981
|
+
return parts.join(" ");
|
|
1982
|
+
}
|
|
1983
|
+
function isRecord(value) {
|
|
1984
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1985
|
+
}
|
|
1986
|
+
/** Extensions for the media types the app can send; anything unknown stages as .png. */
|
|
1987
|
+
const IMAGE_MEDIA_TYPE_EXT = {
|
|
1988
|
+
"image/jpeg": "jpg",
|
|
1989
|
+
"image/png": "png",
|
|
1990
|
+
"image/gif": "gif",
|
|
1991
|
+
"image/webp": "webp",
|
|
1992
|
+
};
|
|
1993
|
+
function extractUserText(message) {
|
|
1994
|
+
const content = message.message?.content;
|
|
1995
|
+
if (typeof content === "string")
|
|
1996
|
+
return content;
|
|
1997
|
+
if (Array.isArray(content)) {
|
|
1998
|
+
return content
|
|
1999
|
+
.filter((b) => !!b && typeof b === "object" && b.type === "text")
|
|
2000
|
+
.map((b) => b.text)
|
|
2001
|
+
.join("");
|
|
2002
|
+
}
|
|
2003
|
+
return "";
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Drop ANSI/OSC sequences and remaining control chars. Scanned by code point rather than a
|
|
2007
|
+
* regex so the source carries no control characters and avoids no-control-regex (same
|
|
2008
|
+
* approach as sanitizeUploadFilename).
|
|
2009
|
+
*/
|
|
2010
|
+
function stripTerminalControl(src) {
|
|
2011
|
+
const ESC = 0x1b;
|
|
2012
|
+
const BEL = 0x07;
|
|
2013
|
+
const out = [];
|
|
2014
|
+
for (let i = 0; i < src.length; i++) {
|
|
2015
|
+
const code = src.charCodeAt(i);
|
|
2016
|
+
if (code === ESC) {
|
|
2017
|
+
const next = src[i + 1];
|
|
2018
|
+
if (next === "[") {
|
|
2019
|
+
i += 2;
|
|
2020
|
+
while (i < src.length && !isCsiFinal(src.charCodeAt(i)))
|
|
2021
|
+
i++;
|
|
2022
|
+
}
|
|
2023
|
+
else if (next === "]") {
|
|
2024
|
+
i += 2;
|
|
2025
|
+
while (i < src.length && src.charCodeAt(i) !== BEL && src.charCodeAt(i) !== ESC)
|
|
2026
|
+
i++;
|
|
2027
|
+
if (i < src.length && src.charCodeAt(i) === ESC)
|
|
2028
|
+
i++;
|
|
2029
|
+
}
|
|
2030
|
+
else {
|
|
2031
|
+
i += 1;
|
|
2032
|
+
}
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (code === 0x0a) {
|
|
2036
|
+
out.push("\n");
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (code === 0x09) {
|
|
2040
|
+
out.push(" ");
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
if (code <= 0x1f || code === 0x7f)
|
|
2044
|
+
continue;
|
|
2045
|
+
out.push(src[i]);
|
|
2046
|
+
}
|
|
2047
|
+
return out.join("").replace(/[ ]{2,}/g, " ");
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* True for the TUI's own progress chrome: spinner frames, the elapsed-time counter and
|
|
2051
|
+
* token tickers. These redraw constantly and carry no diagnostic value, and keeping them
|
|
2052
|
+
* would evict the lines that do.
|
|
2053
|
+
*/
|
|
2054
|
+
function isProgressNoise(line) {
|
|
2055
|
+
if (/^[^a-z]*\d+m?\s*\d*s\)?[^a-z]*$/i.test(line))
|
|
2056
|
+
return true;
|
|
2057
|
+
if (/^\W*(esc to interrupt|ctrl\+[a-z])\W*$/i.test(line))
|
|
2058
|
+
return true;
|
|
2059
|
+
const letters = (line.match(/[a-z]/gi) ?? []).length;
|
|
2060
|
+
return letters < 3;
|
|
2061
|
+
}
|
|
2062
|
+
function isCsiFinal(code) {
|
|
2063
|
+
return code >= 0x40 && code <= 0x7e;
|
|
2064
|
+
}
|
|
2065
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2066
|
+
async function pollUntil(cond, timeoutMs, intervalMs) {
|
|
2067
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
2068
|
+
while (monotonicNowMs() < deadline) {
|
|
2069
|
+
if (cond())
|
|
2070
|
+
return true;
|
|
2071
|
+
await delay(intervalMs);
|
|
2072
|
+
}
|
|
2073
|
+
return cond();
|
|
2074
|
+
}
|
|
2075
|
+
//# sourceMappingURL=pty-query.js.map
|