@getpaseo/server 0.1.2
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 +20 -0
- package/README.md +107 -0
- package/agent-prompt.md +339 -0
- package/dist/scripts/daemon-runner.js +32 -0
- package/dist/scripts/daemon-runner.js.map +1 -0
- package/dist/scripts/dev-runner.js +19 -0
- package/dist/scripts/dev-runner.js.map +1 -0
- package/dist/scripts/mcp-stdio-socket-bridge-cli.mjs +62 -0
- package/dist/scripts/supervisor.js +95 -0
- package/dist/scripts/supervisor.js.map +1 -0
- package/dist/server/client/daemon-client.d.ts +383 -0
- package/dist/server/client/daemon-client.d.ts.map +1 -0
- package/dist/server/client/daemon-client.js +2443 -0
- package/dist/server/client/daemon-client.js.map +1 -0
- package/dist/server/server/agent/activity-curator.d.ts +8 -0
- package/dist/server/server/agent/activity-curator.d.ts.map +1 -0
- package/dist/server/server/agent/activity-curator.js +228 -0
- package/dist/server/server/agent/activity-curator.js.map +1 -0
- package/dist/server/server/agent/agent-management-mcp.d.ts +34 -0
- package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -0
- package/dist/server/server/agent/agent-management-mcp.js +619 -0
- package/dist/server/server/agent/agent-management-mcp.js.map +1 -0
- package/dist/server/server/agent/agent-manager.d.ts +182 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -0
- package/dist/server/server/agent/agent-manager.js +1066 -0
- package/dist/server/server/agent/agent-manager.js.map +1 -0
- package/dist/server/server/agent/agent-metadata-generator.d.ts +29 -0
- package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -0
- package/dist/server/server/agent/agent-metadata-generator.js +157 -0
- package/dist/server/server/agent/agent-metadata-generator.js.map +1 -0
- package/dist/server/server/agent/agent-projections.d.ts +12 -0
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -0
- package/dist/server/server/agent/agent-projections.js +238 -0
- package/dist/server/server/agent/agent-projections.js.map +1 -0
- package/dist/server/server/agent/agent-response-loop.d.ts +32 -0
- package/dist/server/server/agent/agent-response-loop.d.ts.map +1 -0
- package/dist/server/server/agent/agent-response-loop.js +224 -0
- package/dist/server/server/agent/agent-response-loop.js.map +1 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +360 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -0
- package/dist/server/server/agent/agent-sdk-types.js +2 -0
- package/dist/server/server/agent/agent-sdk-types.js.map +1 -0
- package/dist/server/server/agent/agent-storage.d.ts +187 -0
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -0
- package/dist/server/server/agent/agent-storage.js +328 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -0
- package/dist/server/server/agent/audio-utils.d.ts +3 -0
- package/dist/server/server/agent/audio-utils.d.ts.map +1 -0
- package/dist/server/server/agent/audio-utils.js +19 -0
- package/dist/server/server/agent/audio-utils.js.map +1 -0
- package/dist/server/server/agent/dictation-debug.d.ts +13 -0
- package/dist/server/server/agent/dictation-debug.d.ts.map +1 -0
- package/dist/server/server/agent/dictation-debug.js +50 -0
- package/dist/server/server/agent/dictation-debug.js.map +1 -0
- package/dist/server/server/agent/llm-openai.d.ts +7 -0
- package/dist/server/server/agent/llm-openai.d.ts.map +1 -0
- package/dist/server/server/agent/llm-openai.js +8 -0
- package/dist/server/server/agent/llm-openai.js.map +1 -0
- package/dist/server/server/agent/mcp-server.d.ts +26 -0
- package/dist/server/server/agent/mcp-server.d.ts.map +1 -0
- package/dist/server/server/agent/mcp-server.js +762 -0
- package/dist/server/server/agent/mcp-server.js.map +1 -0
- package/dist/server/server/agent/model-resolver.d.ts +11 -0
- package/dist/server/server/agent/model-resolver.d.ts.map +1 -0
- package/dist/server/server/agent/model-resolver.js +21 -0
- package/dist/server/server/agent/model-resolver.js.map +1 -0
- package/dist/server/server/agent/orchestrator-instructions.d.ts +7 -0
- package/dist/server/server/agent/orchestrator-instructions.d.ts.map +1 -0
- package/dist/server/server/agent/orchestrator-instructions.js +51 -0
- package/dist/server/server/agent/orchestrator-instructions.js.map +1 -0
- package/dist/server/server/agent/orchestrator.d.ts +12 -0
- package/dist/server/server/agent/orchestrator.d.ts.map +1 -0
- package/dist/server/server/agent/orchestrator.js +12 -0
- package/dist/server/server/agent/orchestrator.js.map +1 -0
- package/dist/server/server/agent/pcm16-resampler.d.ts +14 -0
- package/dist/server/server/agent/pcm16-resampler.d.ts.map +1 -0
- package/dist/server/server/agent/pcm16-resampler.js +63 -0
- package/dist/server/server/agent/pcm16-resampler.js.map +1 -0
- package/dist/server/server/agent/provider-launch-config.d.ts +139 -0
- package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -0
- package/dist/server/server/agent/provider-launch-config.js +83 -0
- package/dist/server/server/agent/provider-launch-config.js.map +1 -0
- package/dist/server/server/agent/provider-manifest.d.ts +20 -0
- package/dist/server/server/agent/provider-manifest.d.ts.map +1 -0
- package/dist/server/server/agent/provider-manifest.js +97 -0
- package/dist/server/server/agent/provider-manifest.js.map +1 -0
- package/dist/server/server/agent/provider-registry.d.ts +18 -0
- package/dist/server/server/agent/provider-registry.d.ts.map +1 -0
- package/dist/server/server/agent/provider-registry.js +45 -0
- package/dist/server/server/agent/provider-registry.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +42 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +73 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -0
- package/dist/server/server/agent/providers/claude-agent.d.ts +35 -0
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -0
- package/dist/server/server/agent/providers/claude-agent.js +2056 -0
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +13 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +67 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +15 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js +640 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +34 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.js +2476 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -0
- package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts +9 -0
- package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -0
- package/dist/server/server/agent/providers/codex-rollout-timeline.js +486 -0
- package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +33 -0
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +75 -0
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +37 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode-agent.js +822 -0
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +1363 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +534 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +18 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js +119 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -0
- package/dist/server/server/agent/recordings-debug.d.ts +3 -0
- package/dist/server/server/agent/recordings-debug.d.ts.map +1 -0
- package/dist/server/server/agent/recordings-debug.js +19 -0
- package/dist/server/server/agent/recordings-debug.js.map +1 -0
- package/dist/server/server/agent/stt-debug.d.ts +10 -0
- package/dist/server/server/agent/stt-debug.d.ts.map +1 -0
- package/dist/server/server/agent/stt-debug.js +33 -0
- package/dist/server/server/agent/stt-debug.js.map +1 -0
- package/dist/server/server/agent/stt-manager.d.ts +32 -0
- package/dist/server/server/agent/stt-manager.d.ts.map +1 -0
- package/dist/server/server/agent/stt-manager.js +231 -0
- package/dist/server/server/agent/stt-manager.js.map +1 -0
- package/dist/server/server/agent/system-prompt.d.ts +3 -0
- package/dist/server/server/agent/system-prompt.d.ts.map +1 -0
- package/dist/server/server/agent/system-prompt.js +19 -0
- package/dist/server/server/agent/system-prompt.js.map +1 -0
- package/dist/server/server/agent/tool-name-normalization.d.ts +7 -0
- package/dist/server/server/agent/tool-name-normalization.d.ts.map +1 -0
- package/dist/server/server/agent/tool-name-normalization.js +45 -0
- package/dist/server/server/agent/tool-name-normalization.js.map +1 -0
- package/dist/server/server/agent/tts-debug.d.ts +8 -0
- package/dist/server/server/agent/tts-debug.d.ts.map +1 -0
- package/dist/server/server/agent/tts-debug.js +24 -0
- package/dist/server/server/agent/tts-debug.js.map +1 -0
- package/dist/server/server/agent/tts-manager.d.ts +33 -0
- package/dist/server/server/agent/tts-manager.d.ts.map +1 -0
- package/dist/server/server/agent/tts-manager.js +261 -0
- package/dist/server/server/agent/tts-manager.js.map +1 -0
- package/dist/server/server/agent/wait-for-agent-tracker.d.ts +15 -0
- package/dist/server/server/agent/wait-for-agent-tracker.d.ts.map +1 -0
- package/dist/server/server/agent/wait-for-agent-tracker.js +53 -0
- package/dist/server/server/agent/wait-for-agent-tracker.js.map +1 -0
- package/dist/server/server/allowed-hosts.d.ts +13 -0
- package/dist/server/server/allowed-hosts.d.ts.map +1 -0
- package/dist/server/server/allowed-hosts.js +94 -0
- package/dist/server/server/allowed-hosts.js.map +1 -0
- package/dist/server/server/bootstrap.d.ts +49 -0
- package/dist/server/server/bootstrap.d.ts.map +1 -0
- package/dist/server/server/bootstrap.js +483 -0
- package/dist/server/server/bootstrap.js.map +1 -0
- package/dist/server/server/config.d.ts +13 -0
- package/dist/server/server/config.d.ts.map +1 -0
- package/dist/server/server/config.js +84 -0
- package/dist/server/server/config.js.map +1 -0
- package/dist/server/server/connection-offer.d.ts +19 -0
- package/dist/server/server/connection-offer.d.ts.map +1 -0
- package/dist/server/server/connection-offer.js +60 -0
- package/dist/server/server/connection-offer.js.map +1 -0
- package/dist/server/server/daemon-keypair.d.ts +8 -0
- package/dist/server/server/daemon-keypair.d.ts.map +1 -0
- package/dist/server/server/daemon-keypair.js +40 -0
- package/dist/server/server/daemon-keypair.js.map +1 -0
- package/dist/server/server/dictation/dictation-stream-manager.d.ts +76 -0
- package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -0
- package/dist/server/server/dictation/dictation-stream-manager.js +481 -0
- package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -0
- package/dist/server/server/exports.d.ts +11 -0
- package/dist/server/server/exports.d.ts.map +1 -0
- package/dist/server/server/exports.js +11 -0
- package/dist/server/server/exports.js.map +1 -0
- package/dist/server/server/file-download/token-store.d.ts +25 -0
- package/dist/server/server/file-download/token-store.d.ts.map +1 -0
- package/dist/server/server/file-download/token-store.js +40 -0
- package/dist/server/server/file-download/token-store.js.map +1 -0
- package/dist/server/server/file-explorer/service.d.ts +41 -0
- package/dist/server/server/file-explorer/service.d.ts.map +1 -0
- package/dist/server/server/file-explorer/service.js +163 -0
- package/dist/server/server/file-explorer/service.js.map +1 -0
- package/dist/server/server/index.d.ts +2 -0
- package/dist/server/server/index.d.ts.map +1 -0
- package/dist/server/server/index.js +90 -0
- package/dist/server/server/index.js.map +1 -0
- package/dist/server/server/json-utils.d.ts +11 -0
- package/dist/server/server/json-utils.d.ts.map +1 -0
- package/dist/server/server/json-utils.js +45 -0
- package/dist/server/server/json-utils.js.map +1 -0
- package/dist/server/server/logger.d.ts +12 -0
- package/dist/server/server/logger.d.ts.map +1 -0
- package/dist/server/server/logger.js +29 -0
- package/dist/server/server/logger.js.map +1 -0
- package/dist/server/server/messages.d.ts +9 -0
- package/dist/server/server/messages.d.ts.map +1 -0
- package/dist/server/server/messages.js +29 -0
- package/dist/server/server/messages.js.map +1 -0
- package/dist/server/server/pairing-offer.d.ts +16 -0
- package/dist/server/server/pairing-offer.d.ts.map +1 -0
- package/dist/server/server/pairing-offer.js +45 -0
- package/dist/server/server/pairing-offer.js.map +1 -0
- package/dist/server/server/pairing-qr.d.ts +7 -0
- package/dist/server/server/pairing-qr.d.ts.map +1 -0
- package/dist/server/server/pairing-qr.js +45 -0
- package/dist/server/server/pairing-qr.js.map +1 -0
- package/dist/server/server/paseo-home.d.ts +2 -0
- package/dist/server/server/paseo-home.d.ts.map +1 -0
- package/dist/server/server/paseo-home.js +19 -0
- package/dist/server/server/paseo-home.js.map +1 -0
- package/dist/server/server/path-utils.d.ts +3 -0
- package/dist/server/server/path-utils.d.ts.map +1 -0
- package/dist/server/server/path-utils.js +20 -0
- package/dist/server/server/path-utils.js.map +1 -0
- package/dist/server/server/persisted-config.d.ts +500 -0
- package/dist/server/server/persisted-config.d.ts.map +1 -0
- package/dist/server/server/persisted-config.js +212 -0
- package/dist/server/server/persisted-config.js.map +1 -0
- package/dist/server/server/persistence-hooks.d.ts +24 -0
- package/dist/server/server/persistence-hooks.d.ts.map +1 -0
- package/dist/server/server/persistence-hooks.js +60 -0
- package/dist/server/server/persistence-hooks.js.map +1 -0
- package/dist/server/server/pid-lock.d.ts +19 -0
- package/dist/server/server/pid-lock.d.ts.map +1 -0
- package/dist/server/server/pid-lock.js +115 -0
- package/dist/server/server/pid-lock.js.map +1 -0
- package/dist/server/server/push/push-service.d.ts +21 -0
- package/dist/server/server/push/push-service.d.ts.map +1 -0
- package/dist/server/server/push/push-service.js +68 -0
- package/dist/server/server/push/push-service.js.map +1 -0
- package/dist/server/server/push/token-store.d.ts +18 -0
- package/dist/server/server/push/token-store.d.ts.map +1 -0
- package/dist/server/server/push/token-store.js +70 -0
- package/dist/server/server/push/token-store.js.map +1 -0
- package/dist/server/server/relay-transport.d.ts +22 -0
- package/dist/server/server/relay-transport.d.ts.map +1 -0
- package/dist/server/server/relay-transport.js +374 -0
- package/dist/server/server/relay-transport.js.map +1 -0
- package/dist/server/server/server-id.d.ts +17 -0
- package/dist/server/server/server-id.d.ts.map +1 -0
- package/dist/server/server/server-id.js +63 -0
- package/dist/server/server/server-id.js.map +1 -0
- package/dist/server/server/session.d.ts +360 -0
- package/dist/server/server/session.d.ts.map +1 -0
- package/dist/server/server/session.js +4615 -0
- package/dist/server/server/session.js.map +1 -0
- package/dist/server/server/speech/audio.d.ts +10 -0
- package/dist/server/server/speech/audio.d.ts.map +1 -0
- package/dist/server/server/speech/audio.js +101 -0
- package/dist/server/server/speech/audio.js.map +1 -0
- package/dist/server/server/speech/providers/local/config.d.ts +26 -0
- package/dist/server/server/speech/providers/local/config.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/config.js +93 -0
- package/dist/server/server/speech/providers/local/config.js.map +1 -0
- package/dist/server/server/speech/providers/local/models.d.ts +12 -0
- package/dist/server/server/speech/providers/local/models.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/models.js +18 -0
- package/dist/server/server/speech/providers/local/models.js.map +1 -0
- package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.d.ts +24 -0
- package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.js +422 -0
- package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.js.map +1 -0
- package/dist/server/server/speech/providers/local/runtime.d.ts +30 -0
- package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/runtime.js +254 -0
- package/dist/server/server/speech/providers/local/runtime.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +117 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.js +166 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts +17 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +151 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts +28 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +68 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.d.ts +37 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.js +79 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-loader.d.ts +7 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-loader.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-loader.js +11 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-loader.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts +7 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +44 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +131 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +132 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.d.ts +23 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.js +112 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.js.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.d.ts +23 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.js +140 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.js.map +1 -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.d.ts.map +1 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js +95 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js.map +1 -0
- package/dist/server/server/speech/providers/openai/config.d.ts +22 -0
- package/dist/server/server/speech/providers/openai/config.d.ts.map +1 -0
- package/dist/server/server/speech/providers/openai/config.js +94 -0
- package/dist/server/server/speech/providers/openai/config.js.map +1 -0
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts +42 -0
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts.map +1 -0
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.js +165 -0
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.js.map +1 -0
- package/dist/server/server/speech/providers/openai/runtime.d.ts +27 -0
- package/dist/server/server/speech/providers/openai/runtime.d.ts.map +1 -0
- package/dist/server/server/speech/providers/openai/runtime.js +103 -0
- package/dist/server/server/speech/providers/openai/runtime.js.map +1 -0
- package/dist/server/server/speech/providers/openai/stt.d.ts +22 -0
- package/dist/server/server/speech/providers/openai/stt.d.ts.map +1 -0
- package/dist/server/server/speech/providers/openai/stt.js +208 -0
- package/dist/server/server/speech/providers/openai/stt.js.map +1 -0
- package/dist/server/server/speech/providers/openai/tts.d.ts +18 -0
- package/dist/server/server/speech/providers/openai/tts.d.ts.map +1 -0
- package/dist/server/server/speech/providers/openai/tts.js +46 -0
- package/dist/server/server/speech/providers/openai/tts.js.map +1 -0
- package/dist/server/server/speech/speech-config-resolver.d.ts +11 -0
- package/dist/server/server/speech/speech-config-resolver.d.ts.map +1 -0
- package/dist/server/server/speech/speech-config-resolver.js +64 -0
- package/dist/server/server/speech/speech-config-resolver.js.map +1 -0
- package/dist/server/server/speech/speech-provider.d.ts +59 -0
- package/dist/server/server/speech/speech-provider.d.ts.map +1 -0
- package/dist/server/server/speech/speech-provider.js +2 -0
- package/dist/server/server/speech/speech-provider.js.map +1 -0
- package/dist/server/server/speech/speech-runtime.d.ts +20 -0
- package/dist/server/server/speech/speech-runtime.d.ts.map +1 -0
- package/dist/server/server/speech/speech-runtime.js +119 -0
- package/dist/server/server/speech/speech-runtime.js.map +1 -0
- package/dist/server/server/speech/speech-types.d.ts +20 -0
- package/dist/server/server/speech/speech-types.d.ts.map +1 -0
- package/dist/server/server/speech/speech-types.js +7 -0
- package/dist/server/server/speech/speech-types.js.map +1 -0
- package/dist/server/server/terminal-mcp/index.d.ts +4 -0
- package/dist/server/server/terminal-mcp/index.d.ts.map +1 -0
- package/dist/server/server/terminal-mcp/index.js +3 -0
- package/dist/server/server/terminal-mcp/index.js.map +1 -0
- package/dist/server/server/terminal-mcp/server.d.ts +10 -0
- package/dist/server/server/terminal-mcp/server.d.ts.map +1 -0
- package/dist/server/server/terminal-mcp/server.js +217 -0
- package/dist/server/server/terminal-mcp/server.js.map +1 -0
- package/dist/server/server/terminal-mcp/terminal-manager.d.ts +123 -0
- package/dist/server/server/terminal-mcp/terminal-manager.d.ts.map +1 -0
- package/dist/server/server/terminal-mcp/terminal-manager.js +351 -0
- package/dist/server/server/terminal-mcp/terminal-manager.js.map +1 -0
- package/dist/server/server/terminal-mcp/tmux.d.ts +207 -0
- package/dist/server/server/terminal-mcp/tmux.d.ts.map +1 -0
- package/dist/server/server/terminal-mcp/tmux.js +924 -0
- package/dist/server/server/terminal-mcp/tmux.js.map +1 -0
- package/dist/server/server/types.d.ts +5 -0
- package/dist/server/server/types.d.ts.map +1 -0
- package/dist/server/server/types.js +3 -0
- package/dist/server/server/types.js.map +1 -0
- package/dist/server/server/utils/diff-highlighter.d.ts +52 -0
- package/dist/server/server/utils/diff-highlighter.d.ts.map +1 -0
- package/dist/server/server/utils/diff-highlighter.js +244 -0
- package/dist/server/server/utils/diff-highlighter.js.map +1 -0
- package/dist/server/server/utils/syntax-highlighter.d.ts +10 -0
- package/dist/server/server/utils/syntax-highlighter.d.ts.map +1 -0
- package/dist/server/server/utils/syntax-highlighter.js +141 -0
- package/dist/server/server/utils/syntax-highlighter.js.map +1 -0
- package/dist/server/server/voice-config.d.ts +14 -0
- package/dist/server/server/voice-config.d.ts.map +1 -0
- package/dist/server/server/voice-config.js +51 -0
- package/dist/server/server/voice-config.js.map +1 -0
- package/dist/server/server/voice-mcp-bridge-command.d.ts +17 -0
- package/dist/server/server/voice-mcp-bridge-command.d.ts.map +1 -0
- package/dist/server/server/voice-mcp-bridge-command.js +31 -0
- package/dist/server/server/voice-mcp-bridge-command.js.map +1 -0
- package/dist/server/server/voice-mcp-bridge.d.ts +18 -0
- package/dist/server/server/voice-mcp-bridge.d.ts.map +1 -0
- package/dist/server/server/voice-mcp-bridge.js +109 -0
- package/dist/server/server/voice-mcp-bridge.js.map +1 -0
- package/dist/server/server/voice-permission-policy.d.ts +4 -0
- package/dist/server/server/voice-permission-policy.d.ts.map +1 -0
- package/dist/server/server/voice-permission-policy.js +13 -0
- package/dist/server/server/voice-permission-policy.js.map +1 -0
- package/dist/server/server/voice-types.d.ts +17 -0
- package/dist/server/server/voice-types.d.ts.map +1 -0
- package/dist/server/server/voice-types.js +2 -0
- package/dist/server/server/voice-types.js.map +1 -0
- package/dist/server/server/websocket-server.d.ts +80 -0
- package/dist/server/server/websocket-server.d.ts.map +1 -0
- package/dist/server/server/websocket-server.js +447 -0
- package/dist/server/server/websocket-server.js.map +1 -0
- package/dist/server/shared/agent-lifecycle.d.ts +3 -0
- package/dist/server/shared/agent-lifecycle.d.ts.map +1 -0
- package/dist/server/shared/agent-lifecycle.js +8 -0
- package/dist/server/shared/agent-lifecycle.js.map +1 -0
- package/dist/server/shared/connection-offer.d.ts +62 -0
- package/dist/server/shared/connection-offer.d.ts.map +1 -0
- package/dist/server/shared/connection-offer.js +17 -0
- package/dist/server/shared/connection-offer.js.map +1 -0
- package/dist/server/shared/daemon-endpoints.d.ts +19 -0
- package/dist/server/shared/daemon-endpoints.d.ts.map +1 -0
- package/dist/server/shared/daemon-endpoints.js +98 -0
- package/dist/server/shared/daemon-endpoints.js.map +1 -0
- package/dist/server/shared/messages.d.ts +36729 -0
- package/dist/server/shared/messages.d.ts.map +1 -0
- package/dist/server/shared/messages.js +1666 -0
- package/dist/server/shared/messages.js.map +1 -0
- package/dist/server/shared/path-utils.d.ts +2 -0
- package/dist/server/shared/path-utils.d.ts.map +1 -0
- package/dist/server/shared/path-utils.js +16 -0
- package/dist/server/shared/path-utils.js.map +1 -0
- package/dist/server/shared/tool-call-display.d.ts +11 -0
- package/dist/server/shared/tool-call-display.d.ts.map +1 -0
- package/dist/server/shared/tool-call-display.js +82 -0
- package/dist/server/shared/tool-call-display.js.map +1 -0
- package/dist/server/terminal/terminal-manager.d.ts +14 -0
- package/dist/server/terminal/terminal-manager.d.ts.map +1 -0
- package/dist/server/terminal/terminal-manager.js +67 -0
- package/dist/server/terminal/terminal-manager.js.map +1 -0
- package/dist/server/terminal/terminal.d.ts +67 -0
- package/dist/server/terminal/terminal.d.ts.map +1 -0
- package/dist/server/terminal/terminal.js +190 -0
- package/dist/server/terminal/terminal.js.map +1 -0
- package/dist/server/utils/checkout-git.d.ts +138 -0
- package/dist/server/utils/checkout-git.d.ts.map +1 -0
- package/dist/server/utils/checkout-git.js +1079 -0
- package/dist/server/utils/checkout-git.js.map +1 -0
- package/dist/server/utils/path.d.ts +5 -0
- package/dist/server/utils/path.d.ts.map +1 -0
- package/dist/server/utils/path.js +15 -0
- package/dist/server/utils/path.js.map +1 -0
- package/dist/server/utils/project-icon.d.ts +39 -0
- package/dist/server/utils/project-icon.d.ts.map +1 -0
- package/dist/server/utils/project-icon.js +391 -0
- package/dist/server/utils/project-icon.js.map +1 -0
- package/dist/server/utils/worktree-metadata.d.ts +21 -0
- package/dist/server/utils/worktree-metadata.d.ts.map +1 -0
- package/dist/server/utils/worktree-metadata.js +74 -0
- package/dist/server/utils/worktree-metadata.js.map +1 -0
- package/dist/server/utils/worktree.d.ts +95 -0
- package/dist/server/utils/worktree.d.ts.map +1 -0
- package/dist/server/utils/worktree.js +568 -0
- package/dist/server/utils/worktree.js.map +1 -0
- package/package.json +108 -0
|
@@ -0,0 +1,4615 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { stat } from "fs/promises";
|
|
4
|
+
import { exec } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { join, resolve, sep } from "path";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { serializeAgentStreamEvent, } from "./messages.js";
|
|
9
|
+
import { parseAndHighlightDiff } from "./utils/diff-highlighter.js";
|
|
10
|
+
import { TTSManager } from "./agent/tts-manager.js";
|
|
11
|
+
import { STTManager } from "./agent/stt-manager.js";
|
|
12
|
+
import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
|
|
13
|
+
import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
|
|
14
|
+
import { DictationStreamManager, } from "./dictation/dictation-stream-manager.js";
|
|
15
|
+
import { buildConfigOverrides, buildSessionConfig, extractTimestamps, } from "./persistence-hooks.js";
|
|
16
|
+
import { experimental_createMCPClient } from "ai";
|
|
17
|
+
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
|
18
|
+
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
|
19
|
+
import { toAgentPayload } from "./agent/agent-projections.js";
|
|
20
|
+
import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
|
|
21
|
+
import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
|
22
|
+
import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
|
|
23
|
+
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
|
|
24
|
+
import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
|
|
25
|
+
import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
|
|
26
|
+
import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
|
|
27
|
+
import { getProjectIcon } from "../utils/project-icon.js";
|
|
28
|
+
import { expandTilde } from "../utils/path.js";
|
|
29
|
+
import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
|
|
30
|
+
const execAsync = promisify(exec);
|
|
31
|
+
const READ_ONLY_GIT_ENV = {
|
|
32
|
+
...process.env,
|
|
33
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
34
|
+
};
|
|
35
|
+
const pendingAgentInitializations = new Map();
|
|
36
|
+
let restartRequested = false;
|
|
37
|
+
const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
|
|
38
|
+
const RESTART_EXIT_DELAY_MS = 250;
|
|
39
|
+
const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
|
|
40
|
+
const MAX_AGENTS_PER_PROJECT = 5;
|
|
41
|
+
const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
|
|
42
|
+
const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
|
|
43
|
+
/**
|
|
44
|
+
* Default model used for auto-generating commit messages and PR descriptions.
|
|
45
|
+
* Uses Claude Haiku for speed and cost efficiency.
|
|
46
|
+
*/
|
|
47
|
+
const AUTO_GEN_MODEL = "haiku";
|
|
48
|
+
function deriveRemoteProjectKey(remoteUrl) {
|
|
49
|
+
if (!remoteUrl) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const trimmed = remoteUrl.trim();
|
|
53
|
+
if (!trimmed) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
let host = null;
|
|
57
|
+
let path = null;
|
|
58
|
+
const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
|
|
59
|
+
if (scpLike) {
|
|
60
|
+
host = scpLike[1] ?? null;
|
|
61
|
+
path = scpLike[2] ?? null;
|
|
62
|
+
}
|
|
63
|
+
else if (trimmed.includes("://")) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(trimmed);
|
|
66
|
+
host = parsed.hostname || null;
|
|
67
|
+
path = parsed.pathname ? parsed.pathname.replace(/^\//, "") : null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!host || !path) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
let cleanedPath = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
77
|
+
if (cleanedPath.endsWith(".git")) {
|
|
78
|
+
cleanedPath = cleanedPath.slice(0, -4);
|
|
79
|
+
}
|
|
80
|
+
if (!cleanedPath.includes("/")) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const cleanedHost = host.toLowerCase();
|
|
84
|
+
if (cleanedHost === "github.com") {
|
|
85
|
+
return `remote:github.com/${cleanedPath}`;
|
|
86
|
+
}
|
|
87
|
+
return `remote:${cleanedHost}/${cleanedPath}`;
|
|
88
|
+
}
|
|
89
|
+
function deriveProjectGroupingKey(cwd, remoteUrl) {
|
|
90
|
+
const remoteKey = deriveRemoteProjectKey(remoteUrl);
|
|
91
|
+
if (remoteKey) {
|
|
92
|
+
return remoteKey;
|
|
93
|
+
}
|
|
94
|
+
const worktreeMarker = ".paseo/worktrees/";
|
|
95
|
+
const idx = cwd.indexOf(worktreeMarker);
|
|
96
|
+
if (idx !== -1) {
|
|
97
|
+
return cwd.slice(0, idx).replace(/\/$/, "");
|
|
98
|
+
}
|
|
99
|
+
return cwd;
|
|
100
|
+
}
|
|
101
|
+
function deriveProjectGroupingName(projectKey) {
|
|
102
|
+
const githubRemotePrefix = "remote:github.com/";
|
|
103
|
+
if (projectKey.startsWith(githubRemotePrefix)) {
|
|
104
|
+
return projectKey.slice(githubRemotePrefix.length) || projectKey;
|
|
105
|
+
}
|
|
106
|
+
const segments = projectKey.split(/[\\/]/).filter(Boolean);
|
|
107
|
+
return segments[segments.length - 1] || projectKey;
|
|
108
|
+
}
|
|
109
|
+
const PCM_SAMPLE_RATE = 16000;
|
|
110
|
+
const PCM_CHANNELS = 1;
|
|
111
|
+
const PCM_BITS_PER_SAMPLE = 16;
|
|
112
|
+
const PCM_BYTES_PER_MS = (PCM_SAMPLE_RATE * PCM_CHANNELS * (PCM_BITS_PER_SAMPLE / 8)) / 1000;
|
|
113
|
+
const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
|
|
114
|
+
const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
|
|
115
|
+
const VOICE_MODE_INACTIVITY_FLUSH_MS = 4500;
|
|
116
|
+
const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
|
|
117
|
+
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
|
118
|
+
const AgentIdSchema = z.string().uuid();
|
|
119
|
+
const VOICE_MCP_SERVER_NAME = "paseo_voice";
|
|
120
|
+
function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
|
|
121
|
+
const headerSize = 44;
|
|
122
|
+
const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
|
|
123
|
+
const byteRate = (sampleRate * channels * bitsPerSample) / 8;
|
|
124
|
+
const blockAlign = (channels * bitsPerSample) / 8;
|
|
125
|
+
wavBuffer.write("RIFF", 0);
|
|
126
|
+
wavBuffer.writeUInt32LE(36 + pcmBuffer.length, 4);
|
|
127
|
+
wavBuffer.write("WAVE", 8);
|
|
128
|
+
wavBuffer.write("fmt ", 12);
|
|
129
|
+
wavBuffer.writeUInt32LE(16, 16);
|
|
130
|
+
wavBuffer.writeUInt16LE(1, 20);
|
|
131
|
+
wavBuffer.writeUInt16LE(channels, 22);
|
|
132
|
+
wavBuffer.writeUInt32LE(sampleRate, 24);
|
|
133
|
+
wavBuffer.writeUInt32LE(byteRate, 28);
|
|
134
|
+
wavBuffer.writeUInt16LE(blockAlign, 32);
|
|
135
|
+
wavBuffer.writeUInt16LE(bitsPerSample, 34);
|
|
136
|
+
wavBuffer.write("data", 36);
|
|
137
|
+
wavBuffer.writeUInt32LE(pcmBuffer.length, 40);
|
|
138
|
+
pcmBuffer.copy(wavBuffer, 44);
|
|
139
|
+
return wavBuffer;
|
|
140
|
+
}
|
|
141
|
+
function coerceAgentProvider(logger, value, agentId) {
|
|
142
|
+
if (isValidAgentProvider(value)) {
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
logger.warn({ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, `Unknown provider '${value}' for agent ${agentId ?? "unknown"}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`);
|
|
146
|
+
return DEFAULT_AGENT_PROVIDER;
|
|
147
|
+
}
|
|
148
|
+
function toAgentPersistenceHandle(logger, handle) {
|
|
149
|
+
if (!handle) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const provider = handle.provider;
|
|
153
|
+
if (!isValidAgentProvider(provider)) {
|
|
154
|
+
logger.warn({ provider }, `Ignoring persistence handle with unknown provider '${provider}'`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!handle.sessionId) {
|
|
158
|
+
logger.warn("Ignoring persistence handle missing sessionId");
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
provider,
|
|
163
|
+
sessionId: handle.sessionId,
|
|
164
|
+
nativeHandle: handle.nativeHandle,
|
|
165
|
+
metadata: handle.metadata,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Session represents a single connected client session.
|
|
170
|
+
* It owns all state management, orchestration logic, and message processing.
|
|
171
|
+
* Session has no knowledge of WebSockets - it only emits and receives messages.
|
|
172
|
+
*/
|
|
173
|
+
export class Session {
|
|
174
|
+
constructor(options) {
|
|
175
|
+
this.processingPhase = "idle";
|
|
176
|
+
// Voice mode state
|
|
177
|
+
this.isVoiceMode = false;
|
|
178
|
+
this.speechInProgress = false;
|
|
179
|
+
// Audio buffering for interruption handling
|
|
180
|
+
this.pendingAudioSegments = [];
|
|
181
|
+
this.bufferTimeout = null;
|
|
182
|
+
this.voiceModeInactivityTimeout = null;
|
|
183
|
+
this.audioBuffer = null;
|
|
184
|
+
this.activeVoiceDictationId = null;
|
|
185
|
+
this.activeVoiceDictationFormat = null;
|
|
186
|
+
this.activeVoiceDictationNextSeq = 0;
|
|
187
|
+
this.activeVoiceDictationStartPromise = null;
|
|
188
|
+
this.activeVoiceDictationFinalizePromise = null;
|
|
189
|
+
this.activeVoiceDictationResultPromise = null;
|
|
190
|
+
this.activeVoiceDictationResolve = null;
|
|
191
|
+
this.activeVoiceDictationReject = null;
|
|
192
|
+
// Optional TTS debug capture (persisted per utterance)
|
|
193
|
+
this.ttsDebugStreams = new Map();
|
|
194
|
+
// Per-session MCP client and tools
|
|
195
|
+
this.agentMcpClient = null;
|
|
196
|
+
this.agentTools = null;
|
|
197
|
+
this.unsubscribeAgentEvents = null;
|
|
198
|
+
this.agentUpdatesSubscription = null;
|
|
199
|
+
this.projectPlacementCache = new Map();
|
|
200
|
+
this.clientActivity = null;
|
|
201
|
+
this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
|
|
202
|
+
this.terminalSubscriptions = new Map();
|
|
203
|
+
this.checkoutDiffSubscriptions = new Map();
|
|
204
|
+
this.checkoutDiffTargets = new Map();
|
|
205
|
+
this.voiceModeAgentId = null;
|
|
206
|
+
this.voiceModeBaseConfig = null;
|
|
207
|
+
const { clientId, onMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
|
|
208
|
+
this.clientId = clientId;
|
|
209
|
+
this.sessionId = uuidv4();
|
|
210
|
+
this.onMessage = onMessage;
|
|
211
|
+
this.downloadTokenStore = downloadTokenStore;
|
|
212
|
+
this.pushTokenStore = pushTokenStore;
|
|
213
|
+
this.paseoHome = paseoHome;
|
|
214
|
+
this.agentManager = agentManager;
|
|
215
|
+
this.agentStorage = agentStorage;
|
|
216
|
+
this.createAgentMcpTransport = createAgentMcpTransport;
|
|
217
|
+
this.terminalManager = terminalManager;
|
|
218
|
+
this.voiceAgentMcpStdio = voice?.voiceAgentMcpStdio ?? null;
|
|
219
|
+
const configuredModelsDir = dictation?.localModels?.modelsDir?.trim();
|
|
220
|
+
this.localSpeechModelsDir =
|
|
221
|
+
configuredModelsDir && configuredModelsDir.length > 0
|
|
222
|
+
? configuredModelsDir
|
|
223
|
+
: join(this.paseoHome, "models", "local-speech");
|
|
224
|
+
this.defaultLocalSpeechModelIds =
|
|
225
|
+
dictation?.localModels?.defaultModelIds && dictation.localModels.defaultModelIds.length > 0
|
|
226
|
+
? [...new Set(dictation.localModels.defaultModelIds)]
|
|
227
|
+
: ["parakeet-tdt-0.6b-v2-int8", "kokoro-en-v0_19"];
|
|
228
|
+
this.registerVoiceSpeakHandler = voiceBridge?.registerVoiceSpeakHandler;
|
|
229
|
+
this.unregisterVoiceSpeakHandler = voiceBridge?.unregisterVoiceSpeakHandler;
|
|
230
|
+
this.registerVoiceCallerContext = voiceBridge?.registerVoiceCallerContext;
|
|
231
|
+
this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
|
|
232
|
+
this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
|
|
233
|
+
this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
|
|
234
|
+
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
|
235
|
+
this.abortController = new AbortController();
|
|
236
|
+
this.sessionLogger = logger.child({
|
|
237
|
+
module: "session",
|
|
238
|
+
clientId: this.clientId,
|
|
239
|
+
sessionId: this.sessionId,
|
|
240
|
+
});
|
|
241
|
+
this.providerRegistry = buildProviderRegistry(this.sessionLogger, {
|
|
242
|
+
runtimeSettings: this.agentProviderRuntimeSettings,
|
|
243
|
+
});
|
|
244
|
+
// Initialize per-session managers
|
|
245
|
+
this.ttsManager = new TTSManager(this.sessionId, this.sessionLogger, tts);
|
|
246
|
+
this.sttManager = new STTManager(this.sessionId, this.sessionLogger, stt);
|
|
247
|
+
this.dictationStreamManager = new DictationStreamManager({
|
|
248
|
+
logger: this.sessionLogger,
|
|
249
|
+
sessionId: this.sessionId,
|
|
250
|
+
emit: (msg) => this.handleDictationManagerMessage(msg),
|
|
251
|
+
stt: dictation?.stt ?? null,
|
|
252
|
+
finalTimeoutMs: dictation?.finalTimeoutMs,
|
|
253
|
+
});
|
|
254
|
+
this.voiceStreamManager = new DictationStreamManager({
|
|
255
|
+
logger: this.sessionLogger.child({ stream: "voice-internal" }),
|
|
256
|
+
sessionId: this.sessionId,
|
|
257
|
+
emit: (msg) => this.handleDictationManagerMessage(msg),
|
|
258
|
+
stt: stt,
|
|
259
|
+
finalTimeoutMs: dictation?.finalTimeoutMs,
|
|
260
|
+
});
|
|
261
|
+
// Initialize agent MCP client asynchronously
|
|
262
|
+
void this.initializeAgentMcp();
|
|
263
|
+
this.subscribeToAgentEvents();
|
|
264
|
+
this.sessionLogger.info("Session created");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get the client's current activity state
|
|
268
|
+
*/
|
|
269
|
+
getClientActivity() {
|
|
270
|
+
return this.clientActivity;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Send initial state to client after connection
|
|
274
|
+
*/
|
|
275
|
+
async sendInitialState() {
|
|
276
|
+
// No unsolicited agent list hydration. Callers must use fetch_agents_request.
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Normalize a user prompt (with optional image metadata) for AgentManager
|
|
280
|
+
*/
|
|
281
|
+
buildAgentPrompt(text, images) {
|
|
282
|
+
const normalized = text?.trim() ?? "";
|
|
283
|
+
if (!images || images.length === 0) {
|
|
284
|
+
return normalized;
|
|
285
|
+
}
|
|
286
|
+
const blocks = [];
|
|
287
|
+
if (normalized.length > 0) {
|
|
288
|
+
blocks.push({ type: "text", text: normalized });
|
|
289
|
+
}
|
|
290
|
+
for (const image of images) {
|
|
291
|
+
blocks.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
292
|
+
}
|
|
293
|
+
return blocks;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Interrupt the agent's active run so the next prompt starts a fresh turn.
|
|
297
|
+
* Returns once the manager confirms the stream has been cancelled.
|
|
298
|
+
*/
|
|
299
|
+
async interruptAgentIfRunning(agentId) {
|
|
300
|
+
const snapshot = this.agentManager.getAgent(agentId);
|
|
301
|
+
if (!snapshot) {
|
|
302
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
303
|
+
}
|
|
304
|
+
if (snapshot.lifecycle !== "running" && !snapshot.pendingRun) {
|
|
305
|
+
this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, "interruptAgentIfRunning: not running, skipping");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, "interruptAgentIfRunning: interrupting");
|
|
309
|
+
try {
|
|
310
|
+
const t0 = Date.now();
|
|
311
|
+
const cancelled = await this.agentManager.cancelAgentRun(agentId);
|
|
312
|
+
this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, "interruptAgentIfRunning: cancelAgentRun completed");
|
|
313
|
+
if (!cancelled) {
|
|
314
|
+
this.sessionLogger.warn({ agentId }, "interruptAgentIfRunning: reported running but no active run was cancelled");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
hasActiveAgentRun(agentId) {
|
|
322
|
+
if (!agentId) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const snapshot = this.agentManager.getAgent(agentId);
|
|
326
|
+
if (!snapshot) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return snapshot.lifecycle === "running" || Boolean(snapshot.pendingRun);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Start streaming an agent run and forward results via the websocket broadcast
|
|
333
|
+
*/
|
|
334
|
+
startAgentStream(agentId, prompt) {
|
|
335
|
+
this.sessionLogger.info({ agentId }, `Starting agent stream for ${agentId}`);
|
|
336
|
+
let iterator;
|
|
337
|
+
try {
|
|
338
|
+
iterator = this.agentManager.streamAgent(agentId, prompt);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
|
342
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
343
|
+
return { ok: false, error: message };
|
|
344
|
+
}
|
|
345
|
+
void (async () => {
|
|
346
|
+
try {
|
|
347
|
+
for await (const _ of iterator) {
|
|
348
|
+
// Events are forwarded via the session's AgentManager subscription.
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
this.handleAgentRunError(agentId, error, "Agent stream failed");
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
return { ok: true };
|
|
356
|
+
}
|
|
357
|
+
handleAgentRunError(agentId, error, context) {
|
|
358
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
359
|
+
this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
|
|
360
|
+
this.emit({
|
|
361
|
+
type: "activity_log",
|
|
362
|
+
payload: {
|
|
363
|
+
id: uuidv4(),
|
|
364
|
+
timestamp: new Date(),
|
|
365
|
+
type: "error",
|
|
366
|
+
content: `${context}: ${message}`,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Initialize Agent MCP client for this session using in-memory transport
|
|
372
|
+
*/
|
|
373
|
+
async initializeAgentMcp() {
|
|
374
|
+
try {
|
|
375
|
+
// Create an in-memory transport connected to the Agent MCP server
|
|
376
|
+
const transport = await this.createAgentMcpTransport();
|
|
377
|
+
this.agentMcpClient = await experimental_createMCPClient({
|
|
378
|
+
transport,
|
|
379
|
+
});
|
|
380
|
+
this.agentTools = (await this.agentMcpClient.tools());
|
|
381
|
+
const agentToolCount = Object.keys(this.agentTools ?? {}).length;
|
|
382
|
+
this.sessionLogger.info({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Subscribe to AgentManager events and forward them to the client
|
|
390
|
+
*/
|
|
391
|
+
subscribeToAgentEvents() {
|
|
392
|
+
if (this.unsubscribeAgentEvents) {
|
|
393
|
+
this.unsubscribeAgentEvents();
|
|
394
|
+
}
|
|
395
|
+
this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
|
|
396
|
+
if (event.type === "agent_state") {
|
|
397
|
+
void this.forwardAgentUpdate(event.agent);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (this.isVoiceMode &&
|
|
401
|
+
this.voiceModeAgentId === event.agentId &&
|
|
402
|
+
event.event.type === "permission_requested" &&
|
|
403
|
+
isVoicePermissionAllowed(event.event.request)) {
|
|
404
|
+
const requestId = event.event.request.id;
|
|
405
|
+
void this.agentManager
|
|
406
|
+
.respondToPermission(event.agentId, requestId, {
|
|
407
|
+
behavior: "allow",
|
|
408
|
+
})
|
|
409
|
+
.catch((error) => {
|
|
410
|
+
this.sessionLogger.warn({
|
|
411
|
+
err: error,
|
|
412
|
+
agentId: event.agentId,
|
|
413
|
+
requestId,
|
|
414
|
+
}, "Failed to auto-allow speak tool permission in voice mode");
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
|
|
418
|
+
// for the focused agent, with a short grace window while backgrounded.
|
|
419
|
+
//
|
|
420
|
+
// History catch-up is handled via explicit `initialize_agent_request` which emits a
|
|
421
|
+
// batched `agent_stream_snapshot`.
|
|
422
|
+
const activity = this.clientActivity;
|
|
423
|
+
if (activity?.deviceType === "mobile") {
|
|
424
|
+
if (!activity.focusedAgentId) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (activity.focusedAgentId !== event.agentId) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!activity.appVisible) {
|
|
431
|
+
const hiddenForMs = Date.now() - activity.appVisibilityChangedAt.getTime();
|
|
432
|
+
if (hiddenForMs >= this.MOBILE_BACKGROUND_STREAM_GRACE_MS) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const serializedEvent = serializeAgentStreamEvent(event.event);
|
|
438
|
+
if (!serializedEvent) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const payload = {
|
|
442
|
+
agentId: event.agentId,
|
|
443
|
+
event: serializedEvent,
|
|
444
|
+
timestamp: new Date().toISOString(),
|
|
445
|
+
};
|
|
446
|
+
this.emit({
|
|
447
|
+
type: "agent_stream",
|
|
448
|
+
payload,
|
|
449
|
+
});
|
|
450
|
+
if (event.event.type === "permission_requested") {
|
|
451
|
+
this.emit({
|
|
452
|
+
type: "agent_permission_request",
|
|
453
|
+
payload: {
|
|
454
|
+
agentId: event.agentId,
|
|
455
|
+
request: event.event.request,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
else if (event.event.type === "permission_resolved") {
|
|
460
|
+
this.emit({
|
|
461
|
+
type: "agent_permission_resolved",
|
|
462
|
+
payload: {
|
|
463
|
+
agentId: event.agentId,
|
|
464
|
+
requestId: event.event.requestId,
|
|
465
|
+
resolution: event.event.resolution,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// Title updates may be applied asynchronously after agent creation.
|
|
470
|
+
}, { replayState: false });
|
|
471
|
+
}
|
|
472
|
+
async buildAgentPayload(agent) {
|
|
473
|
+
const storedRecord = await this.agentStorage.get(agent.id);
|
|
474
|
+
const title = storedRecord?.title ?? null;
|
|
475
|
+
const payload = toAgentPayload(agent, { title });
|
|
476
|
+
payload.archivedAt = storedRecord?.archivedAt ?? null;
|
|
477
|
+
return payload;
|
|
478
|
+
}
|
|
479
|
+
buildStoredAgentPayload(record) {
|
|
480
|
+
const defaultCapabilities = {
|
|
481
|
+
supportsStreaming: false,
|
|
482
|
+
supportsSessionPersistence: true,
|
|
483
|
+
supportsDynamicModes: false,
|
|
484
|
+
supportsMcpServers: false,
|
|
485
|
+
supportsReasoningStream: false,
|
|
486
|
+
supportsToolInvocations: true,
|
|
487
|
+
};
|
|
488
|
+
const createdAt = new Date(record.createdAt);
|
|
489
|
+
const updatedAt = new Date(record.lastActivityAt ?? record.updatedAt);
|
|
490
|
+
const lastUserMessageAt = record.lastUserMessageAt
|
|
491
|
+
? new Date(record.lastUserMessageAt)
|
|
492
|
+
: null;
|
|
493
|
+
const provider = coerceAgentProvider(this.sessionLogger, record.provider, record.id);
|
|
494
|
+
return {
|
|
495
|
+
id: record.id,
|
|
496
|
+
provider,
|
|
497
|
+
cwd: record.cwd,
|
|
498
|
+
model: record.config?.model ?? null,
|
|
499
|
+
thinkingOptionId: record.config?.thinkingOptionId ?? null,
|
|
500
|
+
createdAt: createdAt.toISOString(),
|
|
501
|
+
updatedAt: updatedAt.toISOString(),
|
|
502
|
+
lastUserMessageAt: lastUserMessageAt ? lastUserMessageAt.toISOString() : null,
|
|
503
|
+
status: record.lastStatus,
|
|
504
|
+
capabilities: defaultCapabilities,
|
|
505
|
+
currentModeId: record.lastModeId ?? null,
|
|
506
|
+
availableModes: [],
|
|
507
|
+
pendingPermissions: [],
|
|
508
|
+
persistence: toAgentPersistenceHandle(this.sessionLogger, record.persistence),
|
|
509
|
+
lastUsage: undefined,
|
|
510
|
+
lastError: undefined,
|
|
511
|
+
title: record.title ?? null,
|
|
512
|
+
requiresAttention: record.requiresAttention ?? false,
|
|
513
|
+
attentionReason: record.attentionReason ?? null,
|
|
514
|
+
attentionTimestamp: record.attentionTimestamp ?? null,
|
|
515
|
+
archivedAt: record.archivedAt ?? null,
|
|
516
|
+
labels: record.labels,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
async ensureAgentLoaded(agentId) {
|
|
520
|
+
const existing = this.agentManager.getAgent(agentId);
|
|
521
|
+
if (existing) {
|
|
522
|
+
return existing;
|
|
523
|
+
}
|
|
524
|
+
const inflight = pendingAgentInitializations.get(agentId);
|
|
525
|
+
if (inflight) {
|
|
526
|
+
return inflight;
|
|
527
|
+
}
|
|
528
|
+
const initPromise = (async () => {
|
|
529
|
+
const record = await this.agentStorage.get(agentId);
|
|
530
|
+
if (!record) {
|
|
531
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
532
|
+
}
|
|
533
|
+
const handle = toAgentPersistenceHandle(this.sessionLogger, record.persistence);
|
|
534
|
+
let snapshot;
|
|
535
|
+
if (handle) {
|
|
536
|
+
snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const config = buildSessionConfig(record);
|
|
540
|
+
snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
|
|
541
|
+
}
|
|
542
|
+
await this.agentManager.hydrateTimelineFromProvider(agentId);
|
|
543
|
+
return this.agentManager.getAgent(agentId) ?? snapshot;
|
|
544
|
+
})();
|
|
545
|
+
pendingAgentInitializations.set(agentId, initPromise);
|
|
546
|
+
try {
|
|
547
|
+
return await initPromise;
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
const current = pendingAgentInitializations.get(agentId);
|
|
551
|
+
if (current === initPromise) {
|
|
552
|
+
pendingAgentInitializations.delete(agentId);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
matchesAgentFilter(agent, filter) {
|
|
557
|
+
if (filter?.agentId && agent.id !== filter.agentId) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (!filter?.labels) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
return Object.entries(filter.labels).every(([key, value]) => agent.labels[key] === value);
|
|
564
|
+
}
|
|
565
|
+
buildFallbackProjectCheckout(cwd) {
|
|
566
|
+
return {
|
|
567
|
+
cwd,
|
|
568
|
+
isGit: false,
|
|
569
|
+
currentBranch: null,
|
|
570
|
+
remoteUrl: null,
|
|
571
|
+
isPaseoOwnedWorktree: false,
|
|
572
|
+
mainRepoRoot: null,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
toProjectCheckoutLite(cwd, status) {
|
|
576
|
+
if (!status.isGit) {
|
|
577
|
+
return this.buildFallbackProjectCheckout(cwd);
|
|
578
|
+
}
|
|
579
|
+
if (status.isPaseoOwnedWorktree) {
|
|
580
|
+
return {
|
|
581
|
+
cwd,
|
|
582
|
+
isGit: true,
|
|
583
|
+
currentBranch: status.currentBranch,
|
|
584
|
+
remoteUrl: status.remoteUrl,
|
|
585
|
+
isPaseoOwnedWorktree: true,
|
|
586
|
+
mainRepoRoot: status.mainRepoRoot,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
cwd,
|
|
591
|
+
isGit: true,
|
|
592
|
+
currentBranch: status.currentBranch,
|
|
593
|
+
remoteUrl: status.remoteUrl,
|
|
594
|
+
isPaseoOwnedWorktree: false,
|
|
595
|
+
mainRepoRoot: null,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async buildProjectPlacement(cwd) {
|
|
599
|
+
const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome })
|
|
600
|
+
.then((status) => this.toProjectCheckoutLite(cwd, status))
|
|
601
|
+
.catch(() => this.buildFallbackProjectCheckout(cwd));
|
|
602
|
+
const projectKey = deriveProjectGroupingKey(cwd, checkout.remoteUrl);
|
|
603
|
+
return {
|
|
604
|
+
projectKey,
|
|
605
|
+
projectName: deriveProjectGroupingName(projectKey),
|
|
606
|
+
checkout,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
getProjectPlacement(cwd) {
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
const cached = this.projectPlacementCache.get(cwd);
|
|
612
|
+
if (cached && cached.expiresAt > now) {
|
|
613
|
+
return cached.promise;
|
|
614
|
+
}
|
|
615
|
+
const promise = this.buildProjectPlacement(cwd);
|
|
616
|
+
this.projectPlacementCache.set(cwd, {
|
|
617
|
+
expiresAt: now + PROJECT_PLACEMENT_CACHE_TTL_MS,
|
|
618
|
+
promise,
|
|
619
|
+
});
|
|
620
|
+
return promise;
|
|
621
|
+
}
|
|
622
|
+
async forwardAgentUpdate(agent) {
|
|
623
|
+
try {
|
|
624
|
+
const subscription = this.agentUpdatesSubscription;
|
|
625
|
+
if (!subscription) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const payload = await this.buildAgentPayload(agent);
|
|
629
|
+
const matches = this.matchesAgentFilter(payload, subscription.filter);
|
|
630
|
+
if (matches) {
|
|
631
|
+
const project = await this.getProjectPlacement(payload.cwd);
|
|
632
|
+
this.emit({
|
|
633
|
+
type: "agent_update",
|
|
634
|
+
payload: { kind: "upsert", agent: payload, project },
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.emit({
|
|
639
|
+
type: "agent_update",
|
|
640
|
+
payload: { kind: "remove", agentId: payload.id },
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
this.sessionLogger.error({ err: error }, "Failed to emit agent update");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Main entry point for processing session messages
|
|
649
|
+
*/
|
|
650
|
+
async handleMessage(msg) {
|
|
651
|
+
try {
|
|
652
|
+
switch (msg.type) {
|
|
653
|
+
case "voice_audio_chunk":
|
|
654
|
+
await this.handleAudioChunk(msg);
|
|
655
|
+
break;
|
|
656
|
+
case "abort_request":
|
|
657
|
+
await this.handleAbort();
|
|
658
|
+
break;
|
|
659
|
+
case "audio_played":
|
|
660
|
+
this.handleAudioPlayed(msg.id);
|
|
661
|
+
break;
|
|
662
|
+
case "fetch_agents_request":
|
|
663
|
+
await this.handleFetchAgents(msg.requestId, msg.filter);
|
|
664
|
+
break;
|
|
665
|
+
case "fetch_agents_grouped_by_project_request":
|
|
666
|
+
await this.handleFetchAgentsGroupedByProject(msg.requestId, msg.filter);
|
|
667
|
+
break;
|
|
668
|
+
case "fetch_agent_request":
|
|
669
|
+
await this.handleFetchAgent(msg.agentId, msg.requestId);
|
|
670
|
+
break;
|
|
671
|
+
case "subscribe_agent_updates":
|
|
672
|
+
this.agentUpdatesSubscription = {
|
|
673
|
+
subscriptionId: msg.subscriptionId,
|
|
674
|
+
filter: msg.filter,
|
|
675
|
+
};
|
|
676
|
+
break;
|
|
677
|
+
case "unsubscribe_agent_updates":
|
|
678
|
+
if (this.agentUpdatesSubscription?.subscriptionId === msg.subscriptionId) {
|
|
679
|
+
this.agentUpdatesSubscription = null;
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
682
|
+
case "delete_agent_request":
|
|
683
|
+
await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
|
|
684
|
+
break;
|
|
685
|
+
case "archive_agent_request":
|
|
686
|
+
await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
|
|
687
|
+
break;
|
|
688
|
+
case "set_voice_mode":
|
|
689
|
+
await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
|
|
690
|
+
break;
|
|
691
|
+
case "send_agent_message_request":
|
|
692
|
+
await this.handleSendAgentMessageRequest(msg);
|
|
693
|
+
break;
|
|
694
|
+
case "wait_for_finish_request":
|
|
695
|
+
await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
|
|
696
|
+
break;
|
|
697
|
+
case "dictation_stream_start":
|
|
698
|
+
await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
|
|
699
|
+
break;
|
|
700
|
+
case "dictation_stream_chunk":
|
|
701
|
+
await this.dictationStreamManager.handleChunk({
|
|
702
|
+
dictationId: msg.dictationId,
|
|
703
|
+
seq: msg.seq,
|
|
704
|
+
audioBase64: msg.audio,
|
|
705
|
+
format: msg.format,
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
case "dictation_stream_finish":
|
|
709
|
+
await this.dictationStreamManager.handleFinish(msg.dictationId, msg.finalSeq);
|
|
710
|
+
break;
|
|
711
|
+
case "dictation_stream_cancel":
|
|
712
|
+
this.dictationStreamManager.handleCancel(msg.dictationId);
|
|
713
|
+
break;
|
|
714
|
+
case "create_agent_request":
|
|
715
|
+
await this.handleCreateAgentRequest(msg);
|
|
716
|
+
break;
|
|
717
|
+
case "resume_agent_request":
|
|
718
|
+
await this.handleResumeAgentRequest(msg);
|
|
719
|
+
break;
|
|
720
|
+
case "refresh_agent_request":
|
|
721
|
+
await this.handleRefreshAgentRequest(msg);
|
|
722
|
+
break;
|
|
723
|
+
case "cancel_agent_request":
|
|
724
|
+
await this.handleCancelAgentRequest(msg.agentId);
|
|
725
|
+
break;
|
|
726
|
+
case "restart_server_request":
|
|
727
|
+
await this.handleRestartServerRequest(msg.requestId, msg.reason);
|
|
728
|
+
break;
|
|
729
|
+
case "initialize_agent_request":
|
|
730
|
+
await this.handleInitializeAgentRequest(msg.agentId, msg.requestId);
|
|
731
|
+
break;
|
|
732
|
+
case "set_agent_mode_request":
|
|
733
|
+
await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
|
|
734
|
+
break;
|
|
735
|
+
case "set_agent_model_request":
|
|
736
|
+
await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId);
|
|
737
|
+
break;
|
|
738
|
+
case "set_agent_thinking_request":
|
|
739
|
+
await this.handleSetAgentThinkingRequest(msg.agentId, msg.thinkingOptionId, msg.requestId);
|
|
740
|
+
break;
|
|
741
|
+
case "agent_permission_response":
|
|
742
|
+
await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
|
|
743
|
+
break;
|
|
744
|
+
case "git_diff_request":
|
|
745
|
+
await this.handleGitDiffRequest(msg.agentId, msg.requestId);
|
|
746
|
+
break;
|
|
747
|
+
case "checkout_status_request":
|
|
748
|
+
await this.handleCheckoutStatusRequest(msg);
|
|
749
|
+
break;
|
|
750
|
+
case "validate_branch_request":
|
|
751
|
+
await this.handleValidateBranchRequest(msg);
|
|
752
|
+
break;
|
|
753
|
+
case "subscribe_checkout_diff_request":
|
|
754
|
+
await this.handleSubscribeCheckoutDiffRequest(msg);
|
|
755
|
+
break;
|
|
756
|
+
case "unsubscribe_checkout_diff_request":
|
|
757
|
+
this.handleUnsubscribeCheckoutDiffRequest(msg);
|
|
758
|
+
break;
|
|
759
|
+
case "checkout_commit_request":
|
|
760
|
+
await this.handleCheckoutCommitRequest(msg);
|
|
761
|
+
break;
|
|
762
|
+
case "checkout_merge_request":
|
|
763
|
+
await this.handleCheckoutMergeRequest(msg);
|
|
764
|
+
break;
|
|
765
|
+
case "checkout_merge_from_base_request":
|
|
766
|
+
await this.handleCheckoutMergeFromBaseRequest(msg);
|
|
767
|
+
break;
|
|
768
|
+
case "checkout_push_request":
|
|
769
|
+
await this.handleCheckoutPushRequest(msg);
|
|
770
|
+
break;
|
|
771
|
+
case "checkout_pr_create_request":
|
|
772
|
+
await this.handleCheckoutPrCreateRequest(msg);
|
|
773
|
+
break;
|
|
774
|
+
case "checkout_pr_status_request":
|
|
775
|
+
await this.handleCheckoutPrStatusRequest(msg);
|
|
776
|
+
break;
|
|
777
|
+
case "paseo_worktree_list_request":
|
|
778
|
+
await this.handlePaseoWorktreeListRequest(msg);
|
|
779
|
+
break;
|
|
780
|
+
case "paseo_worktree_archive_request":
|
|
781
|
+
await this.handlePaseoWorktreeArchiveRequest(msg);
|
|
782
|
+
break;
|
|
783
|
+
case "highlighted_diff_request":
|
|
784
|
+
await this.handleHighlightedDiffRequest(msg.agentId, msg.requestId);
|
|
785
|
+
break;
|
|
786
|
+
case "file_explorer_request":
|
|
787
|
+
await this.handleFileExplorerRequest(msg);
|
|
788
|
+
break;
|
|
789
|
+
case "project_icon_request":
|
|
790
|
+
await this.handleProjectIconRequest(msg);
|
|
791
|
+
break;
|
|
792
|
+
case "file_download_token_request":
|
|
793
|
+
await this.handleFileDownloadTokenRequest(msg);
|
|
794
|
+
break;
|
|
795
|
+
case "list_provider_models_request":
|
|
796
|
+
await this.handleListProviderModelsRequest(msg);
|
|
797
|
+
break;
|
|
798
|
+
case "speech_models_list_request":
|
|
799
|
+
await this.handleSpeechModelsListRequest(msg);
|
|
800
|
+
break;
|
|
801
|
+
case "speech_models_download_request":
|
|
802
|
+
await this.handleSpeechModelsDownloadRequest(msg);
|
|
803
|
+
break;
|
|
804
|
+
case "clear_agent_attention":
|
|
805
|
+
await this.handleClearAgentAttention(msg.agentId);
|
|
806
|
+
break;
|
|
807
|
+
case "client_heartbeat":
|
|
808
|
+
this.handleClientHeartbeat(msg);
|
|
809
|
+
break;
|
|
810
|
+
case "ping": {
|
|
811
|
+
const now = Date.now();
|
|
812
|
+
this.emit({
|
|
813
|
+
type: "pong",
|
|
814
|
+
payload: {
|
|
815
|
+
requestId: msg.requestId,
|
|
816
|
+
clientSentAt: msg.clientSentAt,
|
|
817
|
+
serverReceivedAt: now,
|
|
818
|
+
serverSentAt: now,
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case "list_commands_request":
|
|
824
|
+
await this.handleListCommandsRequest(msg.agentId, msg.requestId);
|
|
825
|
+
break;
|
|
826
|
+
case "execute_command_request":
|
|
827
|
+
await this.handleExecuteCommandRequest(msg.agentId, msg.commandName, msg.args, msg.requestId);
|
|
828
|
+
break;
|
|
829
|
+
case "register_push_token":
|
|
830
|
+
this.handleRegisterPushToken(msg.token);
|
|
831
|
+
break;
|
|
832
|
+
case "list_terminals_request":
|
|
833
|
+
await this.handleListTerminalsRequest(msg);
|
|
834
|
+
break;
|
|
835
|
+
case "create_terminal_request":
|
|
836
|
+
await this.handleCreateTerminalRequest(msg);
|
|
837
|
+
break;
|
|
838
|
+
case "subscribe_terminal_request":
|
|
839
|
+
await this.handleSubscribeTerminalRequest(msg);
|
|
840
|
+
break;
|
|
841
|
+
case "unsubscribe_terminal_request":
|
|
842
|
+
this.handleUnsubscribeTerminalRequest(msg);
|
|
843
|
+
break;
|
|
844
|
+
case "terminal_input":
|
|
845
|
+
this.handleTerminalInput(msg);
|
|
846
|
+
break;
|
|
847
|
+
case "kill_terminal_request":
|
|
848
|
+
await this.handleKillTerminalRequest(msg);
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
854
|
+
this.sessionLogger.error({ err }, "Error handling message");
|
|
855
|
+
const requestId = msg.requestId;
|
|
856
|
+
if (typeof requestId === "string") {
|
|
857
|
+
try {
|
|
858
|
+
this.emit({
|
|
859
|
+
type: "rpc_error",
|
|
860
|
+
payload: {
|
|
861
|
+
requestId,
|
|
862
|
+
requestType: msg.type,
|
|
863
|
+
error: "Request failed",
|
|
864
|
+
code: "handler_error",
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
catch (emitError) {
|
|
869
|
+
this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error");
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this.emit({
|
|
873
|
+
type: "activity_log",
|
|
874
|
+
payload: {
|
|
875
|
+
id: uuidv4(),
|
|
876
|
+
timestamp: new Date(),
|
|
877
|
+
type: "error",
|
|
878
|
+
content: `Error: ${err.message}`,
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async handleRestartServerRequest(requestId, reason) {
|
|
884
|
+
if (restartRequested) {
|
|
885
|
+
this.sessionLogger.debug("Restart already requested, ignoring duplicate");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
restartRequested = true;
|
|
889
|
+
const payload = {
|
|
890
|
+
status: "restart_requested",
|
|
891
|
+
clientId: this.clientId,
|
|
892
|
+
};
|
|
893
|
+
if (reason && reason.trim().length > 0) {
|
|
894
|
+
payload.reason = reason;
|
|
895
|
+
}
|
|
896
|
+
payload.requestId = requestId;
|
|
897
|
+
this.sessionLogger.warn({ reason }, "Restart requested via websocket");
|
|
898
|
+
this.emit({
|
|
899
|
+
type: "status",
|
|
900
|
+
payload,
|
|
901
|
+
});
|
|
902
|
+
if (typeof process.send === "function") {
|
|
903
|
+
process.send({
|
|
904
|
+
type: "paseo:restart",
|
|
905
|
+
...(reason ? { reason } : {}),
|
|
906
|
+
});
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
setTimeout(() => {
|
|
910
|
+
process.exit(0);
|
|
911
|
+
}, RESTART_EXIT_DELAY_MS);
|
|
912
|
+
}
|
|
913
|
+
async handleDeleteAgentRequest(agentId, requestId) {
|
|
914
|
+
this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
|
|
915
|
+
// Prevent the persistence hook from re-creating the record while we close/delete.
|
|
916
|
+
this.agentStorage.beginDelete(agentId);
|
|
917
|
+
try {
|
|
918
|
+
await this.agentManager.closeAgent(agentId);
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
this.sessionLogger.warn({ err: error, agentId }, `Failed to close agent ${agentId} during delete`);
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
await this.agentStorage.remove(agentId);
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
|
|
928
|
+
}
|
|
929
|
+
this.emit({
|
|
930
|
+
type: "agent_deleted",
|
|
931
|
+
payload: {
|
|
932
|
+
agentId,
|
|
933
|
+
requestId,
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
if (this.agentUpdatesSubscription) {
|
|
937
|
+
this.emit({
|
|
938
|
+
type: "agent_update",
|
|
939
|
+
payload: { kind: "remove", agentId },
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async handleArchiveAgentRequest(agentId, requestId) {
|
|
944
|
+
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
|
|
945
|
+
const archivedAt = new Date().toISOString();
|
|
946
|
+
try {
|
|
947
|
+
const existing = await this.agentStorage.get(agentId);
|
|
948
|
+
if (existing) {
|
|
949
|
+
await this.agentStorage.upsert({
|
|
950
|
+
...existing,
|
|
951
|
+
archivedAt,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
this.agentManager.notifyAgentState(agentId);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to archive agent ${agentId}`);
|
|
958
|
+
}
|
|
959
|
+
this.emit({
|
|
960
|
+
type: "agent_archived",
|
|
961
|
+
payload: {
|
|
962
|
+
agentId,
|
|
963
|
+
archivedAt,
|
|
964
|
+
requestId,
|
|
965
|
+
},
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Handle voice mode toggle
|
|
970
|
+
*/
|
|
971
|
+
async handleSetVoiceMode(enabled, agentId, requestId) {
|
|
972
|
+
try {
|
|
973
|
+
if (enabled) {
|
|
974
|
+
const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
|
|
975
|
+
if (this.isVoiceMode &&
|
|
976
|
+
this.voiceModeAgentId &&
|
|
977
|
+
this.voiceModeAgentId !== normalizedAgentId) {
|
|
978
|
+
await this.disableVoiceModeForActiveAgent(true);
|
|
979
|
+
}
|
|
980
|
+
if (!this.isVoiceMode || this.voiceModeAgentId !== normalizedAgentId) {
|
|
981
|
+
const refreshedAgentId = await this.enableVoiceModeForAgent(normalizedAgentId);
|
|
982
|
+
this.voiceModeAgentId = refreshedAgentId;
|
|
983
|
+
}
|
|
984
|
+
this.isVoiceMode = true;
|
|
985
|
+
this.sessionLogger.info({
|
|
986
|
+
agentId: this.voiceModeAgentId,
|
|
987
|
+
}, "Voice mode enabled for existing agent");
|
|
988
|
+
if (requestId) {
|
|
989
|
+
this.emit({
|
|
990
|
+
type: "set_voice_mode_response",
|
|
991
|
+
payload: {
|
|
992
|
+
requestId,
|
|
993
|
+
enabled: true,
|
|
994
|
+
agentId: this.voiceModeAgentId,
|
|
995
|
+
accepted: true,
|
|
996
|
+
error: null,
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
await this.disableVoiceModeForActiveAgent(true);
|
|
1003
|
+
this.isVoiceMode = false;
|
|
1004
|
+
this.sessionLogger.info("Voice mode disabled");
|
|
1005
|
+
if (requestId) {
|
|
1006
|
+
this.emit({
|
|
1007
|
+
type: "set_voice_mode_response",
|
|
1008
|
+
payload: {
|
|
1009
|
+
requestId,
|
|
1010
|
+
enabled: false,
|
|
1011
|
+
agentId: null,
|
|
1012
|
+
accepted: true,
|
|
1013
|
+
error: null,
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
|
|
1020
|
+
this.sessionLogger.error({
|
|
1021
|
+
err: error,
|
|
1022
|
+
enabled,
|
|
1023
|
+
requestedAgentId: agentId ?? null,
|
|
1024
|
+
}, "set_voice_mode failed");
|
|
1025
|
+
if (requestId) {
|
|
1026
|
+
this.emit({
|
|
1027
|
+
type: "set_voice_mode_response",
|
|
1028
|
+
payload: {
|
|
1029
|
+
requestId,
|
|
1030
|
+
enabled: this.isVoiceMode,
|
|
1031
|
+
agentId: this.voiceModeAgentId,
|
|
1032
|
+
accepted: false,
|
|
1033
|
+
error: errorMessage,
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
parseVoiceTargetAgentId(rawId, source) {
|
|
1042
|
+
const parsed = AgentIdSchema.safeParse(rawId.trim());
|
|
1043
|
+
if (!parsed.success) {
|
|
1044
|
+
throw new Error(`${source}: agentId must be a UUID`);
|
|
1045
|
+
}
|
|
1046
|
+
return parsed.data;
|
|
1047
|
+
}
|
|
1048
|
+
cloneMcpServers(servers) {
|
|
1049
|
+
if (!servers) {
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
return JSON.parse(JSON.stringify(servers));
|
|
1053
|
+
}
|
|
1054
|
+
buildVoiceModeMcpServers(existing, socketPath) {
|
|
1055
|
+
const mcpStdio = this.voiceAgentMcpStdio;
|
|
1056
|
+
if (!mcpStdio) {
|
|
1057
|
+
throw new Error("Voice MCP stdio bridge is not configured");
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
...(existing ?? {}),
|
|
1061
|
+
[VOICE_MCP_SERVER_NAME]: buildVoiceAgentMcpServerConfig({
|
|
1062
|
+
command: mcpStdio.command,
|
|
1063
|
+
baseArgs: mcpStdio.baseArgs,
|
|
1064
|
+
socketPath,
|
|
1065
|
+
env: mcpStdio.env,
|
|
1066
|
+
}),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async enableVoiceModeForAgent(agentId) {
|
|
1070
|
+
const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent;
|
|
1071
|
+
if (!ensureVoiceSocket) {
|
|
1072
|
+
throw new Error("Voice MCP socket bridge is not configured");
|
|
1073
|
+
}
|
|
1074
|
+
const existing = await this.ensureAgentLoaded(agentId);
|
|
1075
|
+
const socketPath = await ensureVoiceSocket(agentId);
|
|
1076
|
+
this.registerVoiceBridgeForAgent(agentId);
|
|
1077
|
+
const baseConfig = {
|
|
1078
|
+
systemPrompt: stripVoiceModeSystemPrompt(existing.config.systemPrompt),
|
|
1079
|
+
mcpServers: this.cloneMcpServers(existing.config.mcpServers),
|
|
1080
|
+
};
|
|
1081
|
+
this.voiceModeBaseConfig = baseConfig;
|
|
1082
|
+
const refreshOverrides = {
|
|
1083
|
+
systemPrompt: buildVoiceModeSystemPrompt(baseConfig.systemPrompt, true),
|
|
1084
|
+
mcpServers: this.buildVoiceModeMcpServers(baseConfig.mcpServers, socketPath),
|
|
1085
|
+
};
|
|
1086
|
+
try {
|
|
1087
|
+
const refreshed = await this.agentManager.reloadAgentSession(agentId, refreshOverrides);
|
|
1088
|
+
return refreshed.id;
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
this.unregisterVoiceSpeakHandler?.(agentId);
|
|
1092
|
+
this.unregisterVoiceCallerContext?.(agentId);
|
|
1093
|
+
await this.removeVoiceMcpSocketForAgent?.(agentId).catch(() => undefined);
|
|
1094
|
+
this.voiceModeBaseConfig = null;
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async disableVoiceModeForActiveAgent(restoreAgentConfig) {
|
|
1099
|
+
this.clearVoiceModeInactivityTimeout();
|
|
1100
|
+
this.cancelActiveVoiceDictationStream("voice mode disabled");
|
|
1101
|
+
const agentId = this.voiceModeAgentId;
|
|
1102
|
+
if (!agentId) {
|
|
1103
|
+
this.voiceModeBaseConfig = null;
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
this.unregisterVoiceSpeakHandler?.(agentId);
|
|
1107
|
+
this.unregisterVoiceCallerContext?.(agentId);
|
|
1108
|
+
await this.removeVoiceMcpSocketForAgent?.(agentId).catch((error) => {
|
|
1109
|
+
this.sessionLogger.warn({ err: error, agentId }, "Failed to remove voice MCP socket bridge on disable");
|
|
1110
|
+
});
|
|
1111
|
+
if (restoreAgentConfig && this.voiceModeBaseConfig) {
|
|
1112
|
+
const baseConfig = this.voiceModeBaseConfig;
|
|
1113
|
+
try {
|
|
1114
|
+
await this.agentManager.reloadAgentSession(agentId, {
|
|
1115
|
+
systemPrompt: buildVoiceModeSystemPrompt(baseConfig.systemPrompt, false),
|
|
1116
|
+
mcpServers: this.cloneMcpServers(baseConfig.mcpServers),
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
this.sessionLogger.warn({ err: error, agentId }, "Failed to restore agent config while disabling voice mode");
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
this.voiceModeBaseConfig = null;
|
|
1124
|
+
this.voiceModeAgentId = null;
|
|
1125
|
+
}
|
|
1126
|
+
isInternalVoiceDictationId(dictationId) {
|
|
1127
|
+
return dictationId.startsWith(VOICE_INTERNAL_DICTATION_ID_PREFIX);
|
|
1128
|
+
}
|
|
1129
|
+
handleDictationManagerMessage(msg) {
|
|
1130
|
+
if (msg.type === "activity_log") {
|
|
1131
|
+
const metadata = msg.payload.metadata;
|
|
1132
|
+
const dictationId = metadata && typeof metadata.dictationId === "string" ? metadata.dictationId : null;
|
|
1133
|
+
if (dictationId && this.isInternalVoiceDictationId(dictationId)) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
this.emit(msg);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const payloadWithDictationId = msg.payload;
|
|
1140
|
+
const dictationId = payloadWithDictationId && typeof payloadWithDictationId.dictationId === "string"
|
|
1141
|
+
? payloadWithDictationId.dictationId
|
|
1142
|
+
: null;
|
|
1143
|
+
if (!dictationId || !this.isInternalVoiceDictationId(dictationId)) {
|
|
1144
|
+
this.emit(msg);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (msg.type === "dictation_stream_final") {
|
|
1148
|
+
if (dictationId !== this.activeVoiceDictationId || !this.activeVoiceDictationResolve) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
this.activeVoiceDictationResolve({
|
|
1152
|
+
text: msg.payload.text,
|
|
1153
|
+
...(msg.payload.debugRecordingPath
|
|
1154
|
+
? { debugRecordingPath: msg.payload.debugRecordingPath }
|
|
1155
|
+
: {}),
|
|
1156
|
+
});
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (msg.type === "dictation_stream_error") {
|
|
1160
|
+
if (dictationId !== this.activeVoiceDictationId || !this.activeVoiceDictationReject) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
this.activeVoiceDictationReject(new Error(msg.payload.error));
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
// Ack/partial messages for internal voice dictation are consumed server-side.
|
|
1167
|
+
}
|
|
1168
|
+
resetActiveVoiceDictationState() {
|
|
1169
|
+
this.activeVoiceDictationId = null;
|
|
1170
|
+
this.activeVoiceDictationFormat = null;
|
|
1171
|
+
this.activeVoiceDictationNextSeq = 0;
|
|
1172
|
+
this.activeVoiceDictationStartPromise = null;
|
|
1173
|
+
this.activeVoiceDictationFinalizePromise = null;
|
|
1174
|
+
this.activeVoiceDictationResultPromise = null;
|
|
1175
|
+
this.activeVoiceDictationResolve = null;
|
|
1176
|
+
this.activeVoiceDictationReject = null;
|
|
1177
|
+
}
|
|
1178
|
+
cancelActiveVoiceDictationStream(reason) {
|
|
1179
|
+
const dictationId = this.activeVoiceDictationId;
|
|
1180
|
+
if (!dictationId) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
this.sessionLogger.debug({ dictationId, reason }, "Cancelling active internal voice dictation stream");
|
|
1184
|
+
if (this.activeVoiceDictationReject) {
|
|
1185
|
+
this.activeVoiceDictationReject(new Error(`Voice dictation cancelled: ${reason}`));
|
|
1186
|
+
}
|
|
1187
|
+
this.voiceStreamManager.handleCancel(dictationId);
|
|
1188
|
+
this.resetActiveVoiceDictationState();
|
|
1189
|
+
}
|
|
1190
|
+
async ensureActiveVoiceDictationStream(format) {
|
|
1191
|
+
if (this.activeVoiceDictationId && this.activeVoiceDictationFormat === format) {
|
|
1192
|
+
if (this.activeVoiceDictationStartPromise) {
|
|
1193
|
+
await this.activeVoiceDictationStartPromise;
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (this.activeVoiceDictationId) {
|
|
1198
|
+
await this.finalizeActiveVoiceDictationStream("voice format changed");
|
|
1199
|
+
}
|
|
1200
|
+
const dictationId = `${VOICE_INTERNAL_DICTATION_ID_PREFIX}${uuidv4()}`;
|
|
1201
|
+
let resolve = null;
|
|
1202
|
+
let reject = null;
|
|
1203
|
+
const resultPromise = new Promise((resolveFn, rejectFn) => {
|
|
1204
|
+
resolve = resolveFn;
|
|
1205
|
+
reject = rejectFn;
|
|
1206
|
+
});
|
|
1207
|
+
// Prevent process-level unhandled rejection warnings when cancellation races are resolved later.
|
|
1208
|
+
void resultPromise.catch(() => undefined);
|
|
1209
|
+
this.activeVoiceDictationId = dictationId;
|
|
1210
|
+
this.activeVoiceDictationFormat = format;
|
|
1211
|
+
this.activeVoiceDictationNextSeq = 0;
|
|
1212
|
+
this.activeVoiceDictationFinalizePromise = null;
|
|
1213
|
+
this.activeVoiceDictationResultPromise = resultPromise;
|
|
1214
|
+
this.activeVoiceDictationResolve = resolve;
|
|
1215
|
+
this.activeVoiceDictationReject = reject;
|
|
1216
|
+
this.setPhase("transcribing");
|
|
1217
|
+
this.emit({
|
|
1218
|
+
type: "activity_log",
|
|
1219
|
+
payload: {
|
|
1220
|
+
id: uuidv4(),
|
|
1221
|
+
timestamp: new Date(),
|
|
1222
|
+
type: "system",
|
|
1223
|
+
content: "Transcribing audio...",
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
const startPromise = this.voiceStreamManager.handleStart(dictationId, format);
|
|
1227
|
+
this.activeVoiceDictationStartPromise = startPromise;
|
|
1228
|
+
try {
|
|
1229
|
+
await startPromise;
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
this.resetActiveVoiceDictationState();
|
|
1233
|
+
throw error;
|
|
1234
|
+
}
|
|
1235
|
+
finally {
|
|
1236
|
+
if (this.activeVoiceDictationId === dictationId) {
|
|
1237
|
+
this.activeVoiceDictationStartPromise = null;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async appendToActiveVoiceDictationStream(audioBase64, format) {
|
|
1242
|
+
if (this.activeVoiceDictationFinalizePromise) {
|
|
1243
|
+
await this.activeVoiceDictationFinalizePromise.catch(() => undefined);
|
|
1244
|
+
}
|
|
1245
|
+
await this.ensureActiveVoiceDictationStream(format);
|
|
1246
|
+
const dictationId = this.activeVoiceDictationId;
|
|
1247
|
+
if (!dictationId) {
|
|
1248
|
+
throw new Error("Voice dictation stream did not initialize");
|
|
1249
|
+
}
|
|
1250
|
+
const seq = this.activeVoiceDictationNextSeq;
|
|
1251
|
+
this.activeVoiceDictationNextSeq += 1;
|
|
1252
|
+
await this.voiceStreamManager.handleChunk({
|
|
1253
|
+
dictationId,
|
|
1254
|
+
seq,
|
|
1255
|
+
audioBase64,
|
|
1256
|
+
format,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
async finalizeActiveVoiceDictationStream(reason) {
|
|
1260
|
+
const dictationId = this.activeVoiceDictationId;
|
|
1261
|
+
if (!dictationId) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
this.clearVoiceModeInactivityTimeout();
|
|
1265
|
+
if (this.activeVoiceDictationStartPromise) {
|
|
1266
|
+
await this.activeVoiceDictationStartPromise;
|
|
1267
|
+
}
|
|
1268
|
+
if (this.activeVoiceDictationFinalizePromise) {
|
|
1269
|
+
await this.activeVoiceDictationFinalizePromise;
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const finalSeq = this.activeVoiceDictationNextSeq - 1;
|
|
1273
|
+
const resultPromise = this.activeVoiceDictationResultPromise;
|
|
1274
|
+
if (!resultPromise) {
|
|
1275
|
+
this.resetActiveVoiceDictationState();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
this.activeVoiceDictationFinalizePromise = (async () => {
|
|
1279
|
+
this.sessionLogger.debug({ dictationId, finalSeq, reason }, "Finalizing internal voice dictation stream");
|
|
1280
|
+
await this.voiceStreamManager.handleFinish(dictationId, finalSeq);
|
|
1281
|
+
const result = await resultPromise;
|
|
1282
|
+
this.resetActiveVoiceDictationState();
|
|
1283
|
+
const requestId = uuidv4();
|
|
1284
|
+
const transcriptText = result.text.trim();
|
|
1285
|
+
this.sessionLogger.info({
|
|
1286
|
+
requestId,
|
|
1287
|
+
isVoiceMode: this.isVoiceMode,
|
|
1288
|
+
transcriptLength: transcriptText.length,
|
|
1289
|
+
transcript: transcriptText,
|
|
1290
|
+
}, "Transcription result");
|
|
1291
|
+
await this.handleTranscriptionResultPayload({
|
|
1292
|
+
text: result.text,
|
|
1293
|
+
requestId,
|
|
1294
|
+
...(result.debugRecordingPath
|
|
1295
|
+
? { debugRecordingPath: result.debugRecordingPath, format: "audio/wav" }
|
|
1296
|
+
: {}),
|
|
1297
|
+
});
|
|
1298
|
+
})();
|
|
1299
|
+
try {
|
|
1300
|
+
await this.activeVoiceDictationFinalizePromise;
|
|
1301
|
+
}
|
|
1302
|
+
catch (error) {
|
|
1303
|
+
this.resetActiveVoiceDictationState();
|
|
1304
|
+
this.setPhase("idle");
|
|
1305
|
+
this.clearSpeechInProgress("transcription error");
|
|
1306
|
+
this.emit({
|
|
1307
|
+
type: "activity_log",
|
|
1308
|
+
payload: {
|
|
1309
|
+
id: uuidv4(),
|
|
1310
|
+
timestamp: new Date(),
|
|
1311
|
+
type: "error",
|
|
1312
|
+
content: `Transcription error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1313
|
+
},
|
|
1314
|
+
});
|
|
1315
|
+
throw error;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Handle text message to agent (with optional image attachments)
|
|
1320
|
+
*/
|
|
1321
|
+
async handleSendAgentMessage(agentId, text, messageId, images) {
|
|
1322
|
+
this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ''}`);
|
|
1323
|
+
try {
|
|
1324
|
+
await this.ensureAgentLoaded(agentId);
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt");
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
try {
|
|
1331
|
+
await this.interruptAgentIfRunning(agentId);
|
|
1332
|
+
}
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
this.handleAgentRunError(agentId, error, "Failed to interrupt running agent before sending prompt");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const prompt = this.buildAgentPrompt(text, images);
|
|
1338
|
+
try {
|
|
1339
|
+
this.agentManager.recordUserMessage(agentId, text, { messageId });
|
|
1340
|
+
}
|
|
1341
|
+
catch (error) {
|
|
1342
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
|
|
1343
|
+
}
|
|
1344
|
+
this.startAgentStream(agentId, prompt);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Handle on-demand agent initialization request from client
|
|
1348
|
+
*/
|
|
1349
|
+
async handleInitializeAgentRequest(agentId, requestId) {
|
|
1350
|
+
this.sessionLogger.info({ agentId }, `Initializing agent ${agentId} on demand`);
|
|
1351
|
+
try {
|
|
1352
|
+
const snapshot = await this.ensureAgentLoaded(agentId);
|
|
1353
|
+
await this.forwardAgentUpdate(snapshot);
|
|
1354
|
+
// Send timeline snapshot after hydration (if any)
|
|
1355
|
+
const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
|
|
1356
|
+
this.emit({
|
|
1357
|
+
type: "initialize_agent_request",
|
|
1358
|
+
payload: {
|
|
1359
|
+
agentId,
|
|
1360
|
+
agentStatus: snapshot.lifecycle,
|
|
1361
|
+
timelineSize,
|
|
1362
|
+
requestId,
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
this.sessionLogger.info({ agentId, timelineSize, status: snapshot.lifecycle }, `Agent ${agentId} initialized with ${timelineSize} timeline item(s); status=${snapshot.lifecycle}`);
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to initialize agent ${agentId}`);
|
|
1369
|
+
this.emit({
|
|
1370
|
+
type: "initialize_agent_request",
|
|
1371
|
+
payload: {
|
|
1372
|
+
agentId,
|
|
1373
|
+
requestId,
|
|
1374
|
+
error: error?.message ?? "Failed to initialize agent",
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Handle create agent request
|
|
1381
|
+
*/
|
|
1382
|
+
async handleCreateAgentRequest(msg) {
|
|
1383
|
+
const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg;
|
|
1384
|
+
this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
|
|
1385
|
+
try {
|
|
1386
|
+
const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
|
|
1387
|
+
const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, { labels });
|
|
1388
|
+
await this.forwardAgentUpdate(snapshot);
|
|
1389
|
+
const trimmedPrompt = initialPrompt?.trim();
|
|
1390
|
+
if (trimmedPrompt) {
|
|
1391
|
+
scheduleAgentMetadataGeneration({
|
|
1392
|
+
agentManager: this.agentManager,
|
|
1393
|
+
agentId: snapshot.id,
|
|
1394
|
+
cwd: snapshot.cwd,
|
|
1395
|
+
initialPrompt: trimmedPrompt,
|
|
1396
|
+
explicitTitle: snapshot.config.title,
|
|
1397
|
+
paseoHome: this.paseoHome,
|
|
1398
|
+
logger: this.sessionLogger,
|
|
1399
|
+
});
|
|
1400
|
+
try {
|
|
1401
|
+
await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, uuidv4(), images);
|
|
1402
|
+
}
|
|
1403
|
+
catch (promptError) {
|
|
1404
|
+
this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
|
|
1405
|
+
this.emit({
|
|
1406
|
+
type: "activity_log",
|
|
1407
|
+
payload: {
|
|
1408
|
+
id: uuidv4(),
|
|
1409
|
+
timestamp: new Date(),
|
|
1410
|
+
type: "error",
|
|
1411
|
+
content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (requestId) {
|
|
1417
|
+
const agentPayload = await this.getAgentPayloadById(snapshot.id);
|
|
1418
|
+
if (!agentPayload) {
|
|
1419
|
+
throw new Error(`Agent ${snapshot.id} not found after creation`);
|
|
1420
|
+
}
|
|
1421
|
+
this.emit({
|
|
1422
|
+
type: "status",
|
|
1423
|
+
payload: {
|
|
1424
|
+
status: "agent_created",
|
|
1425
|
+
agentId: snapshot.id,
|
|
1426
|
+
requestId,
|
|
1427
|
+
agent: agentPayload,
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
if (worktreeConfig) {
|
|
1432
|
+
void this.runAsyncWorktreeSetup(snapshot.id, worktreeConfig);
|
|
1433
|
+
}
|
|
1434
|
+
this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
this.sessionLogger.error({ err: error }, "Failed to create agent");
|
|
1438
|
+
if (requestId) {
|
|
1439
|
+
this.emit({
|
|
1440
|
+
type: "status",
|
|
1441
|
+
payload: {
|
|
1442
|
+
status: "agent_create_failed",
|
|
1443
|
+
requestId,
|
|
1444
|
+
error: error?.message ?? String(error),
|
|
1445
|
+
},
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
this.emit({
|
|
1449
|
+
type: "activity_log",
|
|
1450
|
+
payload: {
|
|
1451
|
+
id: uuidv4(),
|
|
1452
|
+
timestamp: new Date(),
|
|
1453
|
+
type: "error",
|
|
1454
|
+
content: `Failed to create agent: ${error.message}`,
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async handleResumeAgentRequest(msg) {
|
|
1460
|
+
const { handle, overrides, requestId } = msg;
|
|
1461
|
+
if (!handle) {
|
|
1462
|
+
this.sessionLogger.warn("Resume request missing persistence handle");
|
|
1463
|
+
this.emit({
|
|
1464
|
+
type: "activity_log",
|
|
1465
|
+
payload: {
|
|
1466
|
+
id: uuidv4(),
|
|
1467
|
+
timestamp: new Date(),
|
|
1468
|
+
type: "error",
|
|
1469
|
+
content: "Unable to resume agent: missing persistence handle",
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
this.sessionLogger.info({ sessionId: handle.sessionId, provider: handle.provider }, `Resuming agent ${handle.sessionId} (${handle.provider})`);
|
|
1475
|
+
try {
|
|
1476
|
+
const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
|
|
1477
|
+
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
|
|
1478
|
+
await this.forwardAgentUpdate(snapshot);
|
|
1479
|
+
const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
|
|
1480
|
+
if (requestId) {
|
|
1481
|
+
const agentPayload = await this.getAgentPayloadById(snapshot.id);
|
|
1482
|
+
if (!agentPayload) {
|
|
1483
|
+
throw new Error(`Agent ${snapshot.id} not found after resume`);
|
|
1484
|
+
}
|
|
1485
|
+
this.emit({
|
|
1486
|
+
type: "status",
|
|
1487
|
+
payload: {
|
|
1488
|
+
status: "agent_resumed",
|
|
1489
|
+
agentId: snapshot.id,
|
|
1490
|
+
requestId,
|
|
1491
|
+
timelineSize,
|
|
1492
|
+
agent: agentPayload,
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
this.sessionLogger.error({ err: error }, "Failed to resume agent");
|
|
1499
|
+
this.emit({
|
|
1500
|
+
type: "activity_log",
|
|
1501
|
+
payload: {
|
|
1502
|
+
id: uuidv4(),
|
|
1503
|
+
timestamp: new Date(),
|
|
1504
|
+
type: "error",
|
|
1505
|
+
content: `Failed to resume agent: ${error.message}`,
|
|
1506
|
+
},
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async handleRefreshAgentRequest(msg) {
|
|
1511
|
+
const { agentId, requestId } = msg;
|
|
1512
|
+
this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
|
|
1513
|
+
try {
|
|
1514
|
+
let snapshot;
|
|
1515
|
+
const existing = this.agentManager.getAgent(agentId);
|
|
1516
|
+
if (existing) {
|
|
1517
|
+
await this.interruptAgentIfRunning(agentId);
|
|
1518
|
+
if (existing.persistence) {
|
|
1519
|
+
snapshot = await this.agentManager.reloadAgentSession(agentId);
|
|
1520
|
+
}
|
|
1521
|
+
else {
|
|
1522
|
+
snapshot = existing;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
const record = await this.agentStorage.get(agentId);
|
|
1527
|
+
if (!record) {
|
|
1528
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
1529
|
+
}
|
|
1530
|
+
const handle = toAgentPersistenceHandle(this.sessionLogger, record.persistence);
|
|
1531
|
+
if (!handle) {
|
|
1532
|
+
throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
|
|
1533
|
+
}
|
|
1534
|
+
snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
|
|
1535
|
+
}
|
|
1536
|
+
await this.agentManager.hydrateTimelineFromProvider(agentId);
|
|
1537
|
+
await this.forwardAgentUpdate(snapshot);
|
|
1538
|
+
const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
|
|
1539
|
+
if (requestId) {
|
|
1540
|
+
this.emit({
|
|
1541
|
+
type: "status",
|
|
1542
|
+
payload: {
|
|
1543
|
+
status: "agent_refreshed",
|
|
1544
|
+
agentId,
|
|
1545
|
+
requestId,
|
|
1546
|
+
timelineSize,
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
catch (error) {
|
|
1552
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
|
|
1553
|
+
this.emit({
|
|
1554
|
+
type: "activity_log",
|
|
1555
|
+
payload: {
|
|
1556
|
+
id: uuidv4(),
|
|
1557
|
+
timestamp: new Date(),
|
|
1558
|
+
type: "error",
|
|
1559
|
+
content: `Failed to refresh agent: ${error.message}`,
|
|
1560
|
+
},
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
async handleCancelAgentRequest(agentId) {
|
|
1565
|
+
this.sessionLogger.info({ agentId }, `Cancel request received for agent ${agentId}`);
|
|
1566
|
+
try {
|
|
1567
|
+
await this.interruptAgentIfRunning(agentId);
|
|
1568
|
+
}
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
|
|
1574
|
+
let cwd = expandTilde(config.cwd);
|
|
1575
|
+
const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);
|
|
1576
|
+
let worktreeConfig;
|
|
1577
|
+
if (!normalized) {
|
|
1578
|
+
return {
|
|
1579
|
+
sessionConfig: {
|
|
1580
|
+
...config,
|
|
1581
|
+
cwd,
|
|
1582
|
+
},
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
if (normalized.createWorktree) {
|
|
1586
|
+
let targetBranch;
|
|
1587
|
+
if (normalized.createNewBranch) {
|
|
1588
|
+
targetBranch = normalized.newBranchName;
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
// Resolve current branch name from HEAD
|
|
1592
|
+
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
|
1593
|
+
cwd,
|
|
1594
|
+
env: READ_ONLY_GIT_ENV,
|
|
1595
|
+
});
|
|
1596
|
+
targetBranch = stdout.trim();
|
|
1597
|
+
}
|
|
1598
|
+
if (!targetBranch) {
|
|
1599
|
+
throw new Error("A branch name is required when creating a worktree.");
|
|
1600
|
+
}
|
|
1601
|
+
this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
|
|
1602
|
+
const createdWorktree = await createWorktree({
|
|
1603
|
+
branchName: targetBranch,
|
|
1604
|
+
cwd,
|
|
1605
|
+
baseBranch: normalized.baseBranch,
|
|
1606
|
+
worktreeSlug: normalized.worktreeSlug ?? targetBranch,
|
|
1607
|
+
runSetup: false,
|
|
1608
|
+
paseoHome: this.paseoHome,
|
|
1609
|
+
});
|
|
1610
|
+
cwd = createdWorktree.worktreePath;
|
|
1611
|
+
worktreeConfig = createdWorktree;
|
|
1612
|
+
}
|
|
1613
|
+
else if (normalized.createNewBranch) {
|
|
1614
|
+
await this.createBranchFromBase({
|
|
1615
|
+
cwd,
|
|
1616
|
+
baseBranch: normalized.baseBranch,
|
|
1617
|
+
newBranchName: normalized.newBranchName,
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
else if (normalized.baseBranch) {
|
|
1621
|
+
await this.checkoutExistingBranch(cwd, normalized.baseBranch);
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
sessionConfig: {
|
|
1625
|
+
...config,
|
|
1626
|
+
cwd,
|
|
1627
|
+
},
|
|
1628
|
+
worktreeConfig,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
async runAsyncWorktreeSetup(agentId, worktree) {
|
|
1632
|
+
const callId = uuidv4();
|
|
1633
|
+
let results = [];
|
|
1634
|
+
try {
|
|
1635
|
+
const started = await this.safeAppendTimelineItem(agentId, {
|
|
1636
|
+
type: "tool_call",
|
|
1637
|
+
name: "paseo_worktree_setup",
|
|
1638
|
+
callId,
|
|
1639
|
+
status: "running",
|
|
1640
|
+
detail: {
|
|
1641
|
+
type: "unknown",
|
|
1642
|
+
input: {
|
|
1643
|
+
worktreePath: worktree.worktreePath,
|
|
1644
|
+
branchName: worktree.branchName,
|
|
1645
|
+
},
|
|
1646
|
+
output: null,
|
|
1647
|
+
},
|
|
1648
|
+
error: null,
|
|
1649
|
+
});
|
|
1650
|
+
if (!started) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
results = await runWorktreeSetupCommands({
|
|
1654
|
+
worktreePath: worktree.worktreePath,
|
|
1655
|
+
branchName: worktree.branchName,
|
|
1656
|
+
cleanupOnFailure: false,
|
|
1657
|
+
});
|
|
1658
|
+
await this.safeAppendTimelineItem(agentId, {
|
|
1659
|
+
type: "tool_call",
|
|
1660
|
+
name: "paseo_worktree_setup",
|
|
1661
|
+
callId,
|
|
1662
|
+
status: "completed",
|
|
1663
|
+
detail: {
|
|
1664
|
+
type: "unknown",
|
|
1665
|
+
input: {
|
|
1666
|
+
worktreePath: worktree.worktreePath,
|
|
1667
|
+
branchName: worktree.branchName,
|
|
1668
|
+
},
|
|
1669
|
+
output: {
|
|
1670
|
+
worktreePath: worktree.worktreePath,
|
|
1671
|
+
commands: results.map((result) => ({
|
|
1672
|
+
command: result.command,
|
|
1673
|
+
cwd: result.cwd,
|
|
1674
|
+
exitCode: result.exitCode,
|
|
1675
|
+
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
|
1676
|
+
})),
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
error: null,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
catch (error) {
|
|
1683
|
+
if (error instanceof WorktreeSetupError) {
|
|
1684
|
+
results = error.results;
|
|
1685
|
+
}
|
|
1686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1687
|
+
await this.safeAppendTimelineItem(agentId, {
|
|
1688
|
+
type: "tool_call",
|
|
1689
|
+
name: "paseo_worktree_setup",
|
|
1690
|
+
callId,
|
|
1691
|
+
status: "failed",
|
|
1692
|
+
detail: {
|
|
1693
|
+
type: "unknown",
|
|
1694
|
+
input: {
|
|
1695
|
+
worktreePath: worktree.worktreePath,
|
|
1696
|
+
branchName: worktree.branchName,
|
|
1697
|
+
},
|
|
1698
|
+
output: {
|
|
1699
|
+
worktreePath: worktree.worktreePath,
|
|
1700
|
+
commands: results.map((result) => ({
|
|
1701
|
+
command: result.command,
|
|
1702
|
+
cwd: result.cwd,
|
|
1703
|
+
exitCode: result.exitCode,
|
|
1704
|
+
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
|
1705
|
+
})),
|
|
1706
|
+
},
|
|
1707
|
+
},
|
|
1708
|
+
error: { message },
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
async safeAppendTimelineItem(agentId, item) {
|
|
1713
|
+
try {
|
|
1714
|
+
await this.agentManager.appendTimelineItem(agentId, item);
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
catch (error) {
|
|
1718
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1719
|
+
if (message.includes("Unknown agent")) {
|
|
1720
|
+
return false;
|
|
1721
|
+
}
|
|
1722
|
+
throw error;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async handleListProviderModelsRequest(msg) {
|
|
1726
|
+
const fetchedAt = new Date().toISOString();
|
|
1727
|
+
try {
|
|
1728
|
+
const models = await this.providerRegistry[msg.provider].fetchModels({
|
|
1729
|
+
cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
|
|
1730
|
+
});
|
|
1731
|
+
this.emit({
|
|
1732
|
+
type: "list_provider_models_response",
|
|
1733
|
+
payload: {
|
|
1734
|
+
provider: msg.provider,
|
|
1735
|
+
models,
|
|
1736
|
+
error: null,
|
|
1737
|
+
fetchedAt,
|
|
1738
|
+
requestId: msg.requestId,
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
|
|
1744
|
+
this.emit({
|
|
1745
|
+
type: "list_provider_models_response",
|
|
1746
|
+
payload: {
|
|
1747
|
+
provider: msg.provider,
|
|
1748
|
+
error: error?.message ?? String(error),
|
|
1749
|
+
fetchedAt,
|
|
1750
|
+
requestId: msg.requestId,
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
async handleSpeechModelsListRequest(msg) {
|
|
1756
|
+
const modelsDir = this.localSpeechModelsDir;
|
|
1757
|
+
const models = await Promise.all(listLocalSpeechModels().map(async (model) => {
|
|
1758
|
+
const modelDir = getLocalSpeechModelDir(modelsDir, model.id);
|
|
1759
|
+
const missingFiles = [];
|
|
1760
|
+
for (const rel of model.requiredFiles) {
|
|
1761
|
+
const filePath = join(modelDir, rel);
|
|
1762
|
+
try {
|
|
1763
|
+
const fileStat = await stat(filePath);
|
|
1764
|
+
if (fileStat.isDirectory()) {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (!fileStat.isFile() || fileStat.size <= 0) {
|
|
1768
|
+
missingFiles.push(rel);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
catch {
|
|
1772
|
+
missingFiles.push(rel);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return {
|
|
1776
|
+
id: model.id,
|
|
1777
|
+
kind: model.kind,
|
|
1778
|
+
description: model.description,
|
|
1779
|
+
modelDir,
|
|
1780
|
+
isDownloaded: missingFiles.length === 0,
|
|
1781
|
+
...(missingFiles.length > 0 ? { missingFiles } : {}),
|
|
1782
|
+
};
|
|
1783
|
+
}));
|
|
1784
|
+
this.emit({
|
|
1785
|
+
type: "speech_models_list_response",
|
|
1786
|
+
payload: {
|
|
1787
|
+
modelsDir,
|
|
1788
|
+
models,
|
|
1789
|
+
requestId: msg.requestId,
|
|
1790
|
+
},
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
async handleSpeechModelsDownloadRequest(msg) {
|
|
1794
|
+
const modelsDir = this.localSpeechModelsDir;
|
|
1795
|
+
const modelIdsRaw = msg.modelIds && msg.modelIds.length > 0
|
|
1796
|
+
? msg.modelIds
|
|
1797
|
+
: this.defaultLocalSpeechModelIds;
|
|
1798
|
+
const allModelIds = new Set(listLocalSpeechModels().map((m) => m.id));
|
|
1799
|
+
const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id));
|
|
1800
|
+
if (invalid.length > 0) {
|
|
1801
|
+
this.emit({
|
|
1802
|
+
type: "speech_models_download_response",
|
|
1803
|
+
payload: {
|
|
1804
|
+
modelsDir,
|
|
1805
|
+
downloadedModelIds: [],
|
|
1806
|
+
error: `Unknown speech model id(s): ${invalid.join(", ")}`,
|
|
1807
|
+
requestId: msg.requestId,
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const modelIds = modelIdsRaw;
|
|
1813
|
+
try {
|
|
1814
|
+
await ensureLocalSpeechModels({
|
|
1815
|
+
modelsDir,
|
|
1816
|
+
modelIds,
|
|
1817
|
+
autoDownload: true,
|
|
1818
|
+
logger: this.sessionLogger,
|
|
1819
|
+
});
|
|
1820
|
+
this.emit({
|
|
1821
|
+
type: "speech_models_download_response",
|
|
1822
|
+
payload: {
|
|
1823
|
+
modelsDir,
|
|
1824
|
+
downloadedModelIds: modelIds,
|
|
1825
|
+
error: null,
|
|
1826
|
+
requestId: msg.requestId,
|
|
1827
|
+
},
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
catch (error) {
|
|
1831
|
+
this.sessionLogger.error({ err: error, modelIds }, "Failed to download speech models");
|
|
1832
|
+
this.emit({
|
|
1833
|
+
type: "speech_models_download_response",
|
|
1834
|
+
payload: {
|
|
1835
|
+
modelsDir,
|
|
1836
|
+
downloadedModelIds: [],
|
|
1837
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1838
|
+
requestId: msg.requestId,
|
|
1839
|
+
},
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
normalizeGitOptions(gitOptions, legacyWorktreeName) {
|
|
1844
|
+
const fallbackOptions = legacyWorktreeName
|
|
1845
|
+
? {
|
|
1846
|
+
createWorktree: true,
|
|
1847
|
+
createNewBranch: true,
|
|
1848
|
+
newBranchName: legacyWorktreeName,
|
|
1849
|
+
worktreeSlug: legacyWorktreeName,
|
|
1850
|
+
}
|
|
1851
|
+
: undefined;
|
|
1852
|
+
const merged = gitOptions ?? fallbackOptions;
|
|
1853
|
+
if (!merged) {
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
const baseBranch = merged.baseBranch?.trim() || undefined;
|
|
1857
|
+
const createWorktree = Boolean(merged.createWorktree);
|
|
1858
|
+
const createNewBranch = Boolean(merged.createNewBranch);
|
|
1859
|
+
const normalizedBranchName = merged.newBranchName
|
|
1860
|
+
? slugify(merged.newBranchName)
|
|
1861
|
+
: undefined;
|
|
1862
|
+
const normalizedWorktreeSlug = merged.worktreeSlug
|
|
1863
|
+
? slugify(merged.worktreeSlug)
|
|
1864
|
+
: normalizedBranchName;
|
|
1865
|
+
if (!createWorktree && !createNewBranch && !baseBranch) {
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
if (baseBranch) {
|
|
1869
|
+
this.assertSafeGitRef(baseBranch, "base branch");
|
|
1870
|
+
}
|
|
1871
|
+
if (createWorktree && !baseBranch) {
|
|
1872
|
+
throw new Error("Base branch is required when creating a worktree");
|
|
1873
|
+
}
|
|
1874
|
+
if (createNewBranch && !baseBranch) {
|
|
1875
|
+
throw new Error("Base branch is required when creating a new branch");
|
|
1876
|
+
}
|
|
1877
|
+
if (createNewBranch) {
|
|
1878
|
+
if (!normalizedBranchName) {
|
|
1879
|
+
throw new Error("New branch name is required");
|
|
1880
|
+
}
|
|
1881
|
+
const validation = validateBranchSlug(normalizedBranchName);
|
|
1882
|
+
if (!validation.valid) {
|
|
1883
|
+
throw new Error(`Invalid branch name: ${validation.error}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
if (normalizedWorktreeSlug) {
|
|
1887
|
+
const validation = validateBranchSlug(normalizedWorktreeSlug);
|
|
1888
|
+
if (!validation.valid) {
|
|
1889
|
+
throw new Error(`Invalid worktree name: ${validation.error}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
baseBranch,
|
|
1894
|
+
createNewBranch,
|
|
1895
|
+
newBranchName: normalizedBranchName,
|
|
1896
|
+
createWorktree,
|
|
1897
|
+
worktreeSlug: normalizedWorktreeSlug,
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
assertSafeGitRef(ref, label) {
|
|
1901
|
+
if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes("..") || ref.includes("@{")) {
|
|
1902
|
+
throw new Error(`Invalid ${label}: ${ref}`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
toCheckoutError(error) {
|
|
1906
|
+
if (error instanceof NotGitRepoError) {
|
|
1907
|
+
return { code: "NOT_GIT_REPO", message: error.message };
|
|
1908
|
+
}
|
|
1909
|
+
if (error instanceof MergeConflictError) {
|
|
1910
|
+
return { code: "MERGE_CONFLICT", message: error.message };
|
|
1911
|
+
}
|
|
1912
|
+
if (error instanceof MergeFromBaseConflictError) {
|
|
1913
|
+
return { code: "MERGE_CONFLICT", message: error.message };
|
|
1914
|
+
}
|
|
1915
|
+
if (error instanceof Error) {
|
|
1916
|
+
return { code: "UNKNOWN", message: error.message };
|
|
1917
|
+
}
|
|
1918
|
+
return { code: "UNKNOWN", message: String(error) };
|
|
1919
|
+
}
|
|
1920
|
+
isPathWithinRoot(rootPath, candidatePath) {
|
|
1921
|
+
const resolvedRoot = resolve(rootPath);
|
|
1922
|
+
const resolvedCandidate = resolve(candidatePath);
|
|
1923
|
+
if (resolvedCandidate === resolvedRoot) {
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
return resolvedCandidate.startsWith(resolvedRoot + sep);
|
|
1927
|
+
}
|
|
1928
|
+
async generateCommitMessage(cwd) {
|
|
1929
|
+
const diff = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }, { paseoHome: this.paseoHome });
|
|
1930
|
+
const schema = z.object({
|
|
1931
|
+
message: z
|
|
1932
|
+
.string()
|
|
1933
|
+
.min(1)
|
|
1934
|
+
.max(72)
|
|
1935
|
+
.describe("Concise git commit message, imperative mood, no trailing period."),
|
|
1936
|
+
});
|
|
1937
|
+
const fileList = diff.structured && diff.structured.length > 0
|
|
1938
|
+
? [
|
|
1939
|
+
"Files changed:",
|
|
1940
|
+
...diff.structured.map((file) => {
|
|
1941
|
+
const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
|
|
1942
|
+
const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
|
|
1943
|
+
return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
|
|
1944
|
+
}),
|
|
1945
|
+
].join("\n")
|
|
1946
|
+
: "Files changed: (unknown)";
|
|
1947
|
+
const maxPatchChars = 120000;
|
|
1948
|
+
const patch = diff.diff.length > maxPatchChars
|
|
1949
|
+
? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
|
|
1950
|
+
: diff.diff;
|
|
1951
|
+
const prompt = [
|
|
1952
|
+
"Write a concise git commit message for the changes below.",
|
|
1953
|
+
"Return JSON only with a single field 'message'.",
|
|
1954
|
+
"",
|
|
1955
|
+
fileList,
|
|
1956
|
+
"",
|
|
1957
|
+
patch.length > 0 ? patch : "(No diff available)",
|
|
1958
|
+
].join("\n");
|
|
1959
|
+
try {
|
|
1960
|
+
const result = await generateStructuredAgentResponse({
|
|
1961
|
+
manager: this.agentManager,
|
|
1962
|
+
agentConfig: {
|
|
1963
|
+
provider: "claude",
|
|
1964
|
+
model: AUTO_GEN_MODEL,
|
|
1965
|
+
cwd,
|
|
1966
|
+
title: "Commit generator",
|
|
1967
|
+
internal: true,
|
|
1968
|
+
},
|
|
1969
|
+
prompt,
|
|
1970
|
+
schema,
|
|
1971
|
+
schemaName: "CommitMessage",
|
|
1972
|
+
maxRetries: 2,
|
|
1973
|
+
});
|
|
1974
|
+
return result.message;
|
|
1975
|
+
}
|
|
1976
|
+
catch (error) {
|
|
1977
|
+
if (error instanceof StructuredAgentResponseError) {
|
|
1978
|
+
return "Update files";
|
|
1979
|
+
}
|
|
1980
|
+
throw error;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async generatePullRequestText(cwd, baseRef) {
|
|
1984
|
+
const diff = await getCheckoutDiff(cwd, {
|
|
1985
|
+
mode: "base",
|
|
1986
|
+
baseRef,
|
|
1987
|
+
includeStructured: true,
|
|
1988
|
+
}, { paseoHome: this.paseoHome });
|
|
1989
|
+
const schema = z.object({
|
|
1990
|
+
title: z.string().min(1).max(72),
|
|
1991
|
+
body: z.string().min(1),
|
|
1992
|
+
});
|
|
1993
|
+
const fileList = diff.structured && diff.structured.length > 0
|
|
1994
|
+
? [
|
|
1995
|
+
"Files changed:",
|
|
1996
|
+
...diff.structured.map((file) => {
|
|
1997
|
+
const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
|
|
1998
|
+
const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
|
|
1999
|
+
return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
|
|
2000
|
+
}),
|
|
2001
|
+
].join("\n")
|
|
2002
|
+
: "Files changed: (unknown)";
|
|
2003
|
+
const maxPatchChars = 200000;
|
|
2004
|
+
const patch = diff.diff.length > maxPatchChars
|
|
2005
|
+
? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
|
|
2006
|
+
: diff.diff;
|
|
2007
|
+
const prompt = [
|
|
2008
|
+
"Write a pull request title and body for the changes below.",
|
|
2009
|
+
"Return JSON only with fields 'title' and 'body'.",
|
|
2010
|
+
"",
|
|
2011
|
+
fileList,
|
|
2012
|
+
"",
|
|
2013
|
+
patch.length > 0 ? patch : "(No diff available)",
|
|
2014
|
+
].join("\n");
|
|
2015
|
+
try {
|
|
2016
|
+
return await generateStructuredAgentResponse({
|
|
2017
|
+
manager: this.agentManager,
|
|
2018
|
+
agentConfig: {
|
|
2019
|
+
provider: "claude",
|
|
2020
|
+
model: AUTO_GEN_MODEL,
|
|
2021
|
+
cwd,
|
|
2022
|
+
title: "PR generator",
|
|
2023
|
+
internal: true,
|
|
2024
|
+
},
|
|
2025
|
+
prompt,
|
|
2026
|
+
schema,
|
|
2027
|
+
schemaName: "PullRequest",
|
|
2028
|
+
maxRetries: 2,
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
catch (error) {
|
|
2032
|
+
if (error instanceof StructuredAgentResponseError) {
|
|
2033
|
+
return {
|
|
2034
|
+
title: "Update changes",
|
|
2035
|
+
body: "Automated PR generated by Paseo.",
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
throw error;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async ensureCleanWorkingTree(cwd) {
|
|
2042
|
+
const dirty = await this.isWorkingTreeDirty(cwd);
|
|
2043
|
+
if (dirty) {
|
|
2044
|
+
throw new Error("Working directory has uncommitted changes. Commit or stash before switching branches.");
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
async isWorkingTreeDirty(cwd) {
|
|
2048
|
+
try {
|
|
2049
|
+
const { stdout } = await execAsync("git status --porcelain", {
|
|
2050
|
+
cwd,
|
|
2051
|
+
env: READ_ONLY_GIT_ENV,
|
|
2052
|
+
});
|
|
2053
|
+
return stdout.trim().length > 0;
|
|
2054
|
+
}
|
|
2055
|
+
catch (error) {
|
|
2056
|
+
throw new Error(`Unable to inspect git status for ${cwd}: ${error.message}`);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
async checkoutExistingBranch(cwd, branch) {
|
|
2060
|
+
this.assertSafeGitRef(branch, "branch");
|
|
2061
|
+
try {
|
|
2062
|
+
await execAsync(`git rev-parse --verify ${branch}`, { cwd });
|
|
2063
|
+
}
|
|
2064
|
+
catch (error) {
|
|
2065
|
+
throw new Error(`Branch not found: ${branch}`);
|
|
2066
|
+
}
|
|
2067
|
+
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
|
2068
|
+
cwd,
|
|
2069
|
+
});
|
|
2070
|
+
const current = stdout.trim();
|
|
2071
|
+
if (current === branch) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
await this.ensureCleanWorkingTree(cwd);
|
|
2075
|
+
await execAsync(`git checkout ${branch}`, { cwd });
|
|
2076
|
+
}
|
|
2077
|
+
async createBranchFromBase(params) {
|
|
2078
|
+
const { cwd, baseBranch, newBranchName } = params;
|
|
2079
|
+
this.assertSafeGitRef(baseBranch, "base branch");
|
|
2080
|
+
try {
|
|
2081
|
+
await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd });
|
|
2082
|
+
}
|
|
2083
|
+
catch (error) {
|
|
2084
|
+
throw new Error(`Base branch not found: ${baseBranch}`);
|
|
2085
|
+
}
|
|
2086
|
+
const exists = await this.doesLocalBranchExist(cwd, newBranchName);
|
|
2087
|
+
if (exists) {
|
|
2088
|
+
throw new Error(`Branch already exists: ${newBranchName}`);
|
|
2089
|
+
}
|
|
2090
|
+
await this.ensureCleanWorkingTree(cwd);
|
|
2091
|
+
await execAsync(`git checkout -b ${newBranchName} ${baseBranch}`, {
|
|
2092
|
+
cwd,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
async doesLocalBranchExist(cwd, branch) {
|
|
2096
|
+
try {
|
|
2097
|
+
await execAsync(`git show-ref --verify --quiet refs/heads/${branch}`, {
|
|
2098
|
+
cwd,
|
|
2099
|
+
});
|
|
2100
|
+
return true;
|
|
2101
|
+
}
|
|
2102
|
+
catch (error) {
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Handle set agent mode request
|
|
2108
|
+
*/
|
|
2109
|
+
async handleSetAgentModeRequest(agentId, modeId, requestId) {
|
|
2110
|
+
this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
|
|
2111
|
+
try {
|
|
2112
|
+
await this.agentManager.setAgentMode(agentId, modeId);
|
|
2113
|
+
this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request success");
|
|
2114
|
+
this.emit({
|
|
2115
|
+
type: "set_agent_mode_response",
|
|
2116
|
+
payload: { requestId, agentId, accepted: true, error: null },
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
catch (error) {
|
|
2120
|
+
this.sessionLogger.error({ err: error, agentId, modeId, requestId }, "session: set_agent_mode_request error");
|
|
2121
|
+
this.emit({
|
|
2122
|
+
type: "activity_log",
|
|
2123
|
+
payload: {
|
|
2124
|
+
id: uuidv4(),
|
|
2125
|
+
timestamp: new Date(),
|
|
2126
|
+
type: "error",
|
|
2127
|
+
content: `Failed to set agent mode: ${error.message}`,
|
|
2128
|
+
},
|
|
2129
|
+
});
|
|
2130
|
+
this.emit({
|
|
2131
|
+
type: "set_agent_mode_response",
|
|
2132
|
+
payload: {
|
|
2133
|
+
requestId,
|
|
2134
|
+
agentId,
|
|
2135
|
+
accepted: false,
|
|
2136
|
+
error: error?.message ? String(error.message) : "Failed to set agent mode",
|
|
2137
|
+
},
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
async handleSetAgentModelRequest(agentId, modelId, requestId) {
|
|
2142
|
+
this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request");
|
|
2143
|
+
try {
|
|
2144
|
+
await this.agentManager.setAgentModel(agentId, modelId);
|
|
2145
|
+
this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request success");
|
|
2146
|
+
this.emit({
|
|
2147
|
+
type: "set_agent_model_response",
|
|
2148
|
+
payload: { requestId, agentId, accepted: true, error: null },
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
catch (error) {
|
|
2152
|
+
this.sessionLogger.error({ err: error, agentId, modelId, requestId }, "session: set_agent_model_request error");
|
|
2153
|
+
this.emit({
|
|
2154
|
+
type: "activity_log",
|
|
2155
|
+
payload: {
|
|
2156
|
+
id: uuidv4(),
|
|
2157
|
+
timestamp: new Date(),
|
|
2158
|
+
type: "error",
|
|
2159
|
+
content: `Failed to set agent model: ${error.message}`,
|
|
2160
|
+
},
|
|
2161
|
+
});
|
|
2162
|
+
this.emit({
|
|
2163
|
+
type: "set_agent_model_response",
|
|
2164
|
+
payload: {
|
|
2165
|
+
requestId,
|
|
2166
|
+
agentId,
|
|
2167
|
+
accepted: false,
|
|
2168
|
+
error: error?.message ? String(error.message) : "Failed to set agent model",
|
|
2169
|
+
},
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
|
|
2174
|
+
this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request");
|
|
2175
|
+
try {
|
|
2176
|
+
await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
|
|
2177
|
+
this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request success");
|
|
2178
|
+
this.emit({
|
|
2179
|
+
type: "set_agent_thinking_response",
|
|
2180
|
+
payload: { requestId, agentId, accepted: true, error: null },
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request error");
|
|
2185
|
+
this.emit({
|
|
2186
|
+
type: "activity_log",
|
|
2187
|
+
payload: {
|
|
2188
|
+
id: uuidv4(),
|
|
2189
|
+
timestamp: new Date(),
|
|
2190
|
+
type: "error",
|
|
2191
|
+
content: `Failed to set agent thinking option: ${error.message}`,
|
|
2192
|
+
},
|
|
2193
|
+
});
|
|
2194
|
+
this.emit({
|
|
2195
|
+
type: "set_agent_thinking_response",
|
|
2196
|
+
payload: {
|
|
2197
|
+
requestId,
|
|
2198
|
+
agentId,
|
|
2199
|
+
accepted: false,
|
|
2200
|
+
error: error?.message
|
|
2201
|
+
? String(error.message)
|
|
2202
|
+
: "Failed to set agent thinking option",
|
|
2203
|
+
},
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Handle clearing agent attention flag
|
|
2209
|
+
*/
|
|
2210
|
+
async handleClearAgentAttention(agentId) {
|
|
2211
|
+
const agentIds = Array.isArray(agentId) ? agentId : [agentId];
|
|
2212
|
+
this.sessionLogger.debug({ agentIds }, `Clearing attention for ${agentIds.length} agent(s): ${agentIds.join(", ")}`);
|
|
2213
|
+
try {
|
|
2214
|
+
await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
|
|
2215
|
+
}
|
|
2216
|
+
catch (error) {
|
|
2217
|
+
this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
|
|
2218
|
+
// Don't throw - this is not critical
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Handle client heartbeat for activity tracking
|
|
2223
|
+
*/
|
|
2224
|
+
handleClientHeartbeat(msg) {
|
|
2225
|
+
const appVisibilityChangedAt = msg.appVisibilityChangedAt
|
|
2226
|
+
? new Date(msg.appVisibilityChangedAt)
|
|
2227
|
+
: new Date(msg.lastActivityAt);
|
|
2228
|
+
this.clientActivity = {
|
|
2229
|
+
deviceType: msg.deviceType,
|
|
2230
|
+
focusedAgentId: msg.focusedAgentId,
|
|
2231
|
+
lastActivityAt: new Date(msg.lastActivityAt),
|
|
2232
|
+
appVisible: msg.appVisible,
|
|
2233
|
+
appVisibilityChangedAt,
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Handle push token registration
|
|
2238
|
+
*/
|
|
2239
|
+
handleRegisterPushToken(token) {
|
|
2240
|
+
this.pushTokenStore.addToken(token);
|
|
2241
|
+
this.sessionLogger.info("Registered push token");
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Handle list commands request for an agent
|
|
2245
|
+
*/
|
|
2246
|
+
async handleListCommandsRequest(agentId, requestId) {
|
|
2247
|
+
this.sessionLogger.debug({ agentId }, `Handling list commands request for agent ${agentId}`);
|
|
2248
|
+
try {
|
|
2249
|
+
const agents = this.agentManager.listAgents();
|
|
2250
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
2251
|
+
if (!agent) {
|
|
2252
|
+
this.emit({
|
|
2253
|
+
type: "list_commands_response",
|
|
2254
|
+
payload: {
|
|
2255
|
+
agentId,
|
|
2256
|
+
commands: [],
|
|
2257
|
+
error: `Agent not found: ${agentId}`,
|
|
2258
|
+
requestId,
|
|
2259
|
+
},
|
|
2260
|
+
});
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
const session = agent.session;
|
|
2264
|
+
if (!session || !session.listCommands) {
|
|
2265
|
+
this.emit({
|
|
2266
|
+
type: "list_commands_response",
|
|
2267
|
+
payload: {
|
|
2268
|
+
agentId,
|
|
2269
|
+
commands: [],
|
|
2270
|
+
error: `Agent does not support listing commands`,
|
|
2271
|
+
requestId,
|
|
2272
|
+
},
|
|
2273
|
+
});
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
const commands = await session.listCommands();
|
|
2277
|
+
this.emit({
|
|
2278
|
+
type: "list_commands_response",
|
|
2279
|
+
payload: {
|
|
2280
|
+
agentId,
|
|
2281
|
+
commands,
|
|
2282
|
+
error: null,
|
|
2283
|
+
requestId,
|
|
2284
|
+
},
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
catch (error) {
|
|
2288
|
+
this.sessionLogger.error({ err: error, agentId }, "Failed to list commands");
|
|
2289
|
+
this.emit({
|
|
2290
|
+
type: "list_commands_response",
|
|
2291
|
+
payload: {
|
|
2292
|
+
agentId,
|
|
2293
|
+
commands: [],
|
|
2294
|
+
error: error.message,
|
|
2295
|
+
requestId,
|
|
2296
|
+
},
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Handle execute command request for an agent
|
|
2302
|
+
*/
|
|
2303
|
+
async handleExecuteCommandRequest(agentId, commandName, args, requestId) {
|
|
2304
|
+
this.sessionLogger.debug({ agentId, commandName }, `Handling execute command request for agent ${agentId}`);
|
|
2305
|
+
try {
|
|
2306
|
+
const agents = this.agentManager.listAgents();
|
|
2307
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
2308
|
+
if (!agent) {
|
|
2309
|
+
this.emit({
|
|
2310
|
+
type: "execute_command_response",
|
|
2311
|
+
payload: {
|
|
2312
|
+
agentId,
|
|
2313
|
+
result: null,
|
|
2314
|
+
error: `Agent not found: ${agentId}`,
|
|
2315
|
+
requestId,
|
|
2316
|
+
},
|
|
2317
|
+
});
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
const session = agent.session;
|
|
2321
|
+
if (!session || !session.executeCommand) {
|
|
2322
|
+
this.emit({
|
|
2323
|
+
type: "execute_command_response",
|
|
2324
|
+
payload: {
|
|
2325
|
+
agentId,
|
|
2326
|
+
result: null,
|
|
2327
|
+
error: `Agent does not support executing commands`,
|
|
2328
|
+
requestId,
|
|
2329
|
+
},
|
|
2330
|
+
});
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
const result = await session.executeCommand(commandName, args);
|
|
2334
|
+
this.emit({
|
|
2335
|
+
type: "execute_command_response",
|
|
2336
|
+
payload: {
|
|
2337
|
+
agentId,
|
|
2338
|
+
result,
|
|
2339
|
+
error: null,
|
|
2340
|
+
requestId,
|
|
2341
|
+
},
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
catch (error) {
|
|
2345
|
+
this.sessionLogger.error({ err: error, agentId, commandName }, "Failed to execute command");
|
|
2346
|
+
this.emit({
|
|
2347
|
+
type: "execute_command_response",
|
|
2348
|
+
payload: {
|
|
2349
|
+
agentId,
|
|
2350
|
+
result: null,
|
|
2351
|
+
error: error.message,
|
|
2352
|
+
requestId,
|
|
2353
|
+
},
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Handle agent permission response from user
|
|
2359
|
+
*/
|
|
2360
|
+
async handleAgentPermissionResponse(agentId, requestId, response) {
|
|
2361
|
+
this.sessionLogger.debug({ agentId, requestId }, `Handling permission response for agent ${agentId}, request ${requestId}`);
|
|
2362
|
+
try {
|
|
2363
|
+
await this.agentManager.respondToPermission(agentId, requestId, response);
|
|
2364
|
+
this.sessionLogger.debug({ agentId }, `Permission response forwarded to agent ${agentId}`);
|
|
2365
|
+
}
|
|
2366
|
+
catch (error) {
|
|
2367
|
+
this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to respond to permission");
|
|
2368
|
+
this.emit({
|
|
2369
|
+
type: "activity_log",
|
|
2370
|
+
payload: {
|
|
2371
|
+
id: uuidv4(),
|
|
2372
|
+
timestamp: new Date(),
|
|
2373
|
+
type: "error",
|
|
2374
|
+
content: `Failed to respond to permission: ${error.message}`,
|
|
2375
|
+
},
|
|
2376
|
+
});
|
|
2377
|
+
throw error;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Handle git diff request for an agent
|
|
2382
|
+
*/
|
|
2383
|
+
async handleGitDiffRequest(agentId, requestId) {
|
|
2384
|
+
this.sessionLogger.debug({ agentId }, `Handling git diff request for agent ${agentId}`);
|
|
2385
|
+
try {
|
|
2386
|
+
const agents = this.agentManager.listAgents();
|
|
2387
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
2388
|
+
if (!agent) {
|
|
2389
|
+
this.emit({
|
|
2390
|
+
type: "git_diff_response",
|
|
2391
|
+
payload: {
|
|
2392
|
+
agentId,
|
|
2393
|
+
diff: "",
|
|
2394
|
+
error: `Agent not found: ${agentId}`,
|
|
2395
|
+
requestId,
|
|
2396
|
+
},
|
|
2397
|
+
});
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const diffResult = await getCheckoutDiff(agent.cwd, { mode: "uncommitted" }, { paseoHome: this.paseoHome });
|
|
2401
|
+
const combinedDiff = diffResult.diff;
|
|
2402
|
+
this.emit({
|
|
2403
|
+
type: "git_diff_response",
|
|
2404
|
+
payload: {
|
|
2405
|
+
agentId,
|
|
2406
|
+
diff: combinedDiff,
|
|
2407
|
+
error: null,
|
|
2408
|
+
requestId,
|
|
2409
|
+
},
|
|
2410
|
+
});
|
|
2411
|
+
this.sessionLogger.debug({ agentId, diffBytes: combinedDiff.length }, `Git diff for agent ${agentId} completed (${combinedDiff.length} bytes)`);
|
|
2412
|
+
}
|
|
2413
|
+
catch (error) {
|
|
2414
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to get git diff for agent ${agentId}`);
|
|
2415
|
+
this.emit({
|
|
2416
|
+
type: "git_diff_response",
|
|
2417
|
+
payload: {
|
|
2418
|
+
agentId,
|
|
2419
|
+
diff: "",
|
|
2420
|
+
error: error.message,
|
|
2421
|
+
requestId,
|
|
2422
|
+
},
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
async handleCheckoutStatusRequest(msg) {
|
|
2427
|
+
const { cwd, requestId } = msg;
|
|
2428
|
+
try {
|
|
2429
|
+
const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
|
|
2430
|
+
if (!status.isGit) {
|
|
2431
|
+
this.emit({
|
|
2432
|
+
type: "checkout_status_response",
|
|
2433
|
+
payload: {
|
|
2434
|
+
cwd,
|
|
2435
|
+
isGit: false,
|
|
2436
|
+
repoRoot: null,
|
|
2437
|
+
currentBranch: null,
|
|
2438
|
+
isDirty: null,
|
|
2439
|
+
baseRef: null,
|
|
2440
|
+
aheadBehind: null,
|
|
2441
|
+
aheadOfOrigin: null,
|
|
2442
|
+
hasRemote: false,
|
|
2443
|
+
remoteUrl: null,
|
|
2444
|
+
isPaseoOwnedWorktree: false,
|
|
2445
|
+
error: null,
|
|
2446
|
+
requestId,
|
|
2447
|
+
},
|
|
2448
|
+
});
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (status.isPaseoOwnedWorktree) {
|
|
2452
|
+
this.emit({
|
|
2453
|
+
type: "checkout_status_response",
|
|
2454
|
+
payload: {
|
|
2455
|
+
cwd,
|
|
2456
|
+
isGit: true,
|
|
2457
|
+
repoRoot: status.repoRoot ?? null,
|
|
2458
|
+
mainRepoRoot: status.mainRepoRoot,
|
|
2459
|
+
currentBranch: status.currentBranch ?? null,
|
|
2460
|
+
isDirty: status.isDirty ?? null,
|
|
2461
|
+
baseRef: status.baseRef,
|
|
2462
|
+
aheadBehind: status.aheadBehind ?? null,
|
|
2463
|
+
aheadOfOrigin: status.aheadOfOrigin ?? null,
|
|
2464
|
+
hasRemote: status.hasRemote,
|
|
2465
|
+
remoteUrl: status.remoteUrl,
|
|
2466
|
+
isPaseoOwnedWorktree: true,
|
|
2467
|
+
error: null,
|
|
2468
|
+
requestId,
|
|
2469
|
+
},
|
|
2470
|
+
});
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
this.emit({
|
|
2474
|
+
type: "checkout_status_response",
|
|
2475
|
+
payload: {
|
|
2476
|
+
cwd,
|
|
2477
|
+
isGit: true,
|
|
2478
|
+
repoRoot: status.repoRoot ?? null,
|
|
2479
|
+
currentBranch: status.currentBranch ?? null,
|
|
2480
|
+
isDirty: status.isDirty ?? null,
|
|
2481
|
+
baseRef: status.baseRef ?? null,
|
|
2482
|
+
aheadBehind: status.aheadBehind ?? null,
|
|
2483
|
+
aheadOfOrigin: status.aheadOfOrigin ?? null,
|
|
2484
|
+
hasRemote: status.hasRemote,
|
|
2485
|
+
remoteUrl: status.remoteUrl,
|
|
2486
|
+
isPaseoOwnedWorktree: false,
|
|
2487
|
+
error: null,
|
|
2488
|
+
requestId,
|
|
2489
|
+
},
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
catch (error) {
|
|
2493
|
+
this.emit({
|
|
2494
|
+
type: "checkout_status_response",
|
|
2495
|
+
payload: {
|
|
2496
|
+
cwd,
|
|
2497
|
+
isGit: false,
|
|
2498
|
+
repoRoot: null,
|
|
2499
|
+
currentBranch: null,
|
|
2500
|
+
isDirty: null,
|
|
2501
|
+
baseRef: null,
|
|
2502
|
+
aheadBehind: null,
|
|
2503
|
+
aheadOfOrigin: null,
|
|
2504
|
+
hasRemote: false,
|
|
2505
|
+
remoteUrl: null,
|
|
2506
|
+
isPaseoOwnedWorktree: false,
|
|
2507
|
+
error: this.toCheckoutError(error),
|
|
2508
|
+
requestId,
|
|
2509
|
+
},
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
async handleValidateBranchRequest(msg) {
|
|
2514
|
+
const { cwd, branchName, requestId } = msg;
|
|
2515
|
+
try {
|
|
2516
|
+
const resolvedCwd = expandTilde(cwd);
|
|
2517
|
+
// Try local branch first
|
|
2518
|
+
try {
|
|
2519
|
+
await execAsync(`git rev-parse --verify ${branchName}`, {
|
|
2520
|
+
cwd: resolvedCwd,
|
|
2521
|
+
env: READ_ONLY_GIT_ENV,
|
|
2522
|
+
});
|
|
2523
|
+
this.emit({
|
|
2524
|
+
type: "validate_branch_response",
|
|
2525
|
+
payload: {
|
|
2526
|
+
exists: true,
|
|
2527
|
+
resolvedRef: branchName,
|
|
2528
|
+
isRemote: false,
|
|
2529
|
+
error: null,
|
|
2530
|
+
requestId,
|
|
2531
|
+
},
|
|
2532
|
+
});
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
catch {
|
|
2536
|
+
// Local branch doesn't exist, try remote
|
|
2537
|
+
}
|
|
2538
|
+
// Try remote branch (origin/{branchName})
|
|
2539
|
+
try {
|
|
2540
|
+
await execAsync(`git rev-parse --verify origin/${branchName}`, {
|
|
2541
|
+
cwd: resolvedCwd,
|
|
2542
|
+
env: READ_ONLY_GIT_ENV,
|
|
2543
|
+
});
|
|
2544
|
+
this.emit({
|
|
2545
|
+
type: "validate_branch_response",
|
|
2546
|
+
payload: {
|
|
2547
|
+
exists: true,
|
|
2548
|
+
resolvedRef: `origin/${branchName}`,
|
|
2549
|
+
isRemote: true,
|
|
2550
|
+
error: null,
|
|
2551
|
+
requestId,
|
|
2552
|
+
},
|
|
2553
|
+
});
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
catch {
|
|
2557
|
+
// Remote branch doesn't exist either
|
|
2558
|
+
}
|
|
2559
|
+
// Branch not found anywhere
|
|
2560
|
+
this.emit({
|
|
2561
|
+
type: "validate_branch_response",
|
|
2562
|
+
payload: {
|
|
2563
|
+
exists: false,
|
|
2564
|
+
resolvedRef: null,
|
|
2565
|
+
isRemote: false,
|
|
2566
|
+
error: null,
|
|
2567
|
+
requestId,
|
|
2568
|
+
},
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
catch (error) {
|
|
2572
|
+
this.emit({
|
|
2573
|
+
type: "validate_branch_response",
|
|
2574
|
+
payload: {
|
|
2575
|
+
exists: false,
|
|
2576
|
+
resolvedRef: null,
|
|
2577
|
+
isRemote: false,
|
|
2578
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2579
|
+
requestId,
|
|
2580
|
+
},
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
normalizeCheckoutDiffCompare(compare) {
|
|
2585
|
+
if (compare.mode === "uncommitted") {
|
|
2586
|
+
return { mode: "uncommitted" };
|
|
2587
|
+
}
|
|
2588
|
+
const trimmedBaseRef = compare.baseRef?.trim();
|
|
2589
|
+
return trimmedBaseRef
|
|
2590
|
+
? { mode: "base", baseRef: trimmedBaseRef }
|
|
2591
|
+
: { mode: "base" };
|
|
2592
|
+
}
|
|
2593
|
+
buildCheckoutDiffTargetKey(cwd, compare) {
|
|
2594
|
+
return JSON.stringify([
|
|
2595
|
+
cwd,
|
|
2596
|
+
compare.mode,
|
|
2597
|
+
compare.mode === "base" ? (compare.baseRef ?? "") : "",
|
|
2598
|
+
]);
|
|
2599
|
+
}
|
|
2600
|
+
closeCheckoutDiffWatchTarget(target) {
|
|
2601
|
+
if (target.debounceTimer) {
|
|
2602
|
+
clearTimeout(target.debounceTimer);
|
|
2603
|
+
target.debounceTimer = null;
|
|
2604
|
+
}
|
|
2605
|
+
if (target.fallbackRefreshInterval) {
|
|
2606
|
+
clearInterval(target.fallbackRefreshInterval);
|
|
2607
|
+
target.fallbackRefreshInterval = null;
|
|
2608
|
+
}
|
|
2609
|
+
for (const watcher of target.watchers) {
|
|
2610
|
+
watcher.close();
|
|
2611
|
+
}
|
|
2612
|
+
target.watchers = [];
|
|
2613
|
+
}
|
|
2614
|
+
removeCheckoutDiffSubscription(subscriptionId) {
|
|
2615
|
+
const subscription = this.checkoutDiffSubscriptions.get(subscriptionId);
|
|
2616
|
+
if (!subscription) {
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
this.checkoutDiffSubscriptions.delete(subscriptionId);
|
|
2620
|
+
const target = this.checkoutDiffTargets.get(subscription.targetKey);
|
|
2621
|
+
if (!target) {
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
target.subscriptions.delete(subscriptionId);
|
|
2625
|
+
if (target.subscriptions.size === 0) {
|
|
2626
|
+
this.closeCheckoutDiffWatchTarget(target);
|
|
2627
|
+
this.checkoutDiffTargets.delete(subscription.targetKey);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async resolveCheckoutGitDir(cwd) {
|
|
2631
|
+
try {
|
|
2632
|
+
const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
|
|
2633
|
+
cwd,
|
|
2634
|
+
env: READ_ONLY_GIT_ENV,
|
|
2635
|
+
});
|
|
2636
|
+
const gitDir = stdout.trim();
|
|
2637
|
+
return gitDir.length > 0 ? gitDir : null;
|
|
2638
|
+
}
|
|
2639
|
+
catch {
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
async resolveCheckoutWatchRoot(cwd) {
|
|
2644
|
+
try {
|
|
2645
|
+
const { stdout } = await execAsync("git rev-parse --path-format=absolute --show-toplevel", {
|
|
2646
|
+
cwd,
|
|
2647
|
+
env: READ_ONLY_GIT_ENV,
|
|
2648
|
+
});
|
|
2649
|
+
const root = stdout.trim();
|
|
2650
|
+
return root.length > 0 ? root : null;
|
|
2651
|
+
}
|
|
2652
|
+
catch {
|
|
2653
|
+
return null;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
scheduleCheckoutDiffTargetRefresh(target) {
|
|
2657
|
+
if (target.debounceTimer) {
|
|
2658
|
+
clearTimeout(target.debounceTimer);
|
|
2659
|
+
}
|
|
2660
|
+
target.debounceTimer = setTimeout(() => {
|
|
2661
|
+
target.debounceTimer = null;
|
|
2662
|
+
void this.refreshCheckoutDiffTarget(target);
|
|
2663
|
+
}, CHECKOUT_DIFF_WATCH_DEBOUNCE_MS);
|
|
2664
|
+
}
|
|
2665
|
+
emitCheckoutDiffUpdate(target, snapshot) {
|
|
2666
|
+
if (target.subscriptions.size === 0) {
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
for (const subscriptionId of target.subscriptions) {
|
|
2670
|
+
this.emit({
|
|
2671
|
+
type: "checkout_diff_update",
|
|
2672
|
+
payload: {
|
|
2673
|
+
subscriptionId,
|
|
2674
|
+
...snapshot,
|
|
2675
|
+
},
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
checkoutDiffSnapshotFingerprint(snapshot) {
|
|
2680
|
+
return JSON.stringify(snapshot);
|
|
2681
|
+
}
|
|
2682
|
+
async computeCheckoutDiffSnapshot(cwd, compare, options) {
|
|
2683
|
+
const diffCwd = options?.diffCwd ?? cwd;
|
|
2684
|
+
try {
|
|
2685
|
+
const diffResult = await getCheckoutDiff(diffCwd, {
|
|
2686
|
+
mode: compare.mode,
|
|
2687
|
+
baseRef: compare.baseRef,
|
|
2688
|
+
includeStructured: true,
|
|
2689
|
+
}, { paseoHome: this.paseoHome });
|
|
2690
|
+
const files = [...(diffResult.structured ?? [])];
|
|
2691
|
+
files.sort((a, b) => {
|
|
2692
|
+
if (a.path === b.path)
|
|
2693
|
+
return 0;
|
|
2694
|
+
return a.path < b.path ? -1 : 1;
|
|
2695
|
+
});
|
|
2696
|
+
return {
|
|
2697
|
+
cwd,
|
|
2698
|
+
files,
|
|
2699
|
+
error: null,
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
catch (error) {
|
|
2703
|
+
return {
|
|
2704
|
+
cwd,
|
|
2705
|
+
files: [],
|
|
2706
|
+
error: this.toCheckoutError(error),
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
async refreshCheckoutDiffTarget(target) {
|
|
2711
|
+
if (target.refreshPromise) {
|
|
2712
|
+
target.refreshQueued = true;
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
target.refreshPromise = (async () => {
|
|
2716
|
+
do {
|
|
2717
|
+
target.refreshQueued = false;
|
|
2718
|
+
const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, { diffCwd: target.diffCwd });
|
|
2719
|
+
target.latestPayload = snapshot;
|
|
2720
|
+
const fingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
|
|
2721
|
+
if (fingerprint !== target.latestFingerprint) {
|
|
2722
|
+
target.latestFingerprint = fingerprint;
|
|
2723
|
+
this.emitCheckoutDiffUpdate(target, snapshot);
|
|
2724
|
+
}
|
|
2725
|
+
} while (target.refreshQueued);
|
|
2726
|
+
})();
|
|
2727
|
+
try {
|
|
2728
|
+
await target.refreshPromise;
|
|
2729
|
+
}
|
|
2730
|
+
finally {
|
|
2731
|
+
target.refreshPromise = null;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
async ensureCheckoutDiffWatchTarget(cwd, compare) {
|
|
2735
|
+
const targetKey = this.buildCheckoutDiffTargetKey(cwd, compare);
|
|
2736
|
+
const existing = this.checkoutDiffTargets.get(targetKey);
|
|
2737
|
+
if (existing) {
|
|
2738
|
+
return existing;
|
|
2739
|
+
}
|
|
2740
|
+
const watchRoot = await this.resolveCheckoutWatchRoot(cwd);
|
|
2741
|
+
const target = {
|
|
2742
|
+
key: targetKey,
|
|
2743
|
+
cwd,
|
|
2744
|
+
diffCwd: watchRoot ?? cwd,
|
|
2745
|
+
compare,
|
|
2746
|
+
subscriptions: new Set(),
|
|
2747
|
+
watchers: [],
|
|
2748
|
+
fallbackRefreshInterval: null,
|
|
2749
|
+
debounceTimer: null,
|
|
2750
|
+
refreshPromise: null,
|
|
2751
|
+
refreshQueued: false,
|
|
2752
|
+
latestPayload: null,
|
|
2753
|
+
latestFingerprint: null,
|
|
2754
|
+
};
|
|
2755
|
+
const watchPaths = new Set([cwd]);
|
|
2756
|
+
if (watchRoot) {
|
|
2757
|
+
watchPaths.add(watchRoot);
|
|
2758
|
+
}
|
|
2759
|
+
const gitDir = await this.resolveCheckoutGitDir(cwd);
|
|
2760
|
+
if (gitDir) {
|
|
2761
|
+
watchPaths.add(gitDir);
|
|
2762
|
+
}
|
|
2763
|
+
let hasWatchRootCoverage = false;
|
|
2764
|
+
for (const watchPath of watchPaths) {
|
|
2765
|
+
const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
|
|
2766
|
+
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
2767
|
+
});
|
|
2768
|
+
let watcher = null;
|
|
2769
|
+
try {
|
|
2770
|
+
watcher = createWatcher(true);
|
|
2771
|
+
}
|
|
2772
|
+
catch (error) {
|
|
2773
|
+
try {
|
|
2774
|
+
watcher = createWatcher(false);
|
|
2775
|
+
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
|
|
2776
|
+
}
|
|
2777
|
+
catch (fallbackError) {
|
|
2778
|
+
this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (!watcher) {
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
watcher.on("error", (error) => {
|
|
2785
|
+
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
|
|
2786
|
+
});
|
|
2787
|
+
target.watchers.push(watcher);
|
|
2788
|
+
if (watchRoot && watchPath === watchRoot) {
|
|
2789
|
+
hasWatchRootCoverage = true;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
const missingRepoCoverage = Boolean(watchRoot) && !hasWatchRootCoverage;
|
|
2793
|
+
if (target.watchers.length === 0 || missingRepoCoverage) {
|
|
2794
|
+
target.fallbackRefreshInterval = setInterval(() => {
|
|
2795
|
+
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
2796
|
+
}, CHECKOUT_DIFF_FALLBACK_REFRESH_MS);
|
|
2797
|
+
this.sessionLogger.warn({
|
|
2798
|
+
cwd,
|
|
2799
|
+
compare,
|
|
2800
|
+
intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
|
|
2801
|
+
reason: target.watchers.length === 0
|
|
2802
|
+
? "no_watchers"
|
|
2803
|
+
: "missing_repo_root_coverage",
|
|
2804
|
+
}, "Checkout diff watchers unavailable; using timed refresh fallback");
|
|
2805
|
+
}
|
|
2806
|
+
this.checkoutDiffTargets.set(targetKey, target);
|
|
2807
|
+
return target;
|
|
2808
|
+
}
|
|
2809
|
+
async handleSubscribeCheckoutDiffRequest(msg) {
|
|
2810
|
+
const cwd = expandTilde(msg.cwd);
|
|
2811
|
+
const compare = this.normalizeCheckoutDiffCompare(msg.compare);
|
|
2812
|
+
this.removeCheckoutDiffSubscription(msg.subscriptionId);
|
|
2813
|
+
const target = await this.ensureCheckoutDiffWatchTarget(cwd, compare);
|
|
2814
|
+
target.subscriptions.add(msg.subscriptionId);
|
|
2815
|
+
this.checkoutDiffSubscriptions.set(msg.subscriptionId, {
|
|
2816
|
+
targetKey: target.key,
|
|
2817
|
+
});
|
|
2818
|
+
const snapshot = target.latestPayload ??
|
|
2819
|
+
(await this.computeCheckoutDiffSnapshot(cwd, compare, {
|
|
2820
|
+
diffCwd: target.diffCwd,
|
|
2821
|
+
}));
|
|
2822
|
+
target.latestPayload = snapshot;
|
|
2823
|
+
target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
|
|
2824
|
+
this.emit({
|
|
2825
|
+
type: "subscribe_checkout_diff_response",
|
|
2826
|
+
payload: {
|
|
2827
|
+
subscriptionId: msg.subscriptionId,
|
|
2828
|
+
...snapshot,
|
|
2829
|
+
requestId: msg.requestId,
|
|
2830
|
+
},
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
handleUnsubscribeCheckoutDiffRequest(msg) {
|
|
2834
|
+
this.removeCheckoutDiffSubscription(msg.subscriptionId);
|
|
2835
|
+
}
|
|
2836
|
+
scheduleCheckoutDiffRefreshForCwd(cwd) {
|
|
2837
|
+
const resolvedCwd = expandTilde(cwd);
|
|
2838
|
+
for (const target of this.checkoutDiffTargets.values()) {
|
|
2839
|
+
if (target.cwd !== resolvedCwd && target.diffCwd !== resolvedCwd) {
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
async handleCheckoutCommitRequest(msg) {
|
|
2846
|
+
const { cwd, requestId } = msg;
|
|
2847
|
+
try {
|
|
2848
|
+
let message = msg.message?.trim() ?? "";
|
|
2849
|
+
if (!message) {
|
|
2850
|
+
message = await this.generateCommitMessage(cwd);
|
|
2851
|
+
}
|
|
2852
|
+
if (!message) {
|
|
2853
|
+
throw new Error("Commit message is required");
|
|
2854
|
+
}
|
|
2855
|
+
await commitChanges(cwd, {
|
|
2856
|
+
message,
|
|
2857
|
+
addAll: msg.addAll ?? true,
|
|
2858
|
+
});
|
|
2859
|
+
this.scheduleCheckoutDiffRefreshForCwd(cwd);
|
|
2860
|
+
this.emit({
|
|
2861
|
+
type: "checkout_commit_response",
|
|
2862
|
+
payload: {
|
|
2863
|
+
cwd,
|
|
2864
|
+
success: true,
|
|
2865
|
+
error: null,
|
|
2866
|
+
requestId,
|
|
2867
|
+
},
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
catch (error) {
|
|
2871
|
+
this.emit({
|
|
2872
|
+
type: "checkout_commit_response",
|
|
2873
|
+
payload: {
|
|
2874
|
+
cwd,
|
|
2875
|
+
success: false,
|
|
2876
|
+
error: this.toCheckoutError(error),
|
|
2877
|
+
requestId,
|
|
2878
|
+
},
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
async handleCheckoutMergeRequest(msg) {
|
|
2883
|
+
const { cwd, requestId } = msg;
|
|
2884
|
+
try {
|
|
2885
|
+
const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
|
|
2886
|
+
if (!status.isGit) {
|
|
2887
|
+
try {
|
|
2888
|
+
await execAsync("git rev-parse --is-inside-work-tree", {
|
|
2889
|
+
cwd,
|
|
2890
|
+
env: READ_ONLY_GIT_ENV,
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
catch (error) {
|
|
2894
|
+
const details = typeof error?.stderr === "string"
|
|
2895
|
+
? String(error.stderr).trim()
|
|
2896
|
+
: error instanceof Error
|
|
2897
|
+
? error.message
|
|
2898
|
+
: String(error);
|
|
2899
|
+
throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (msg.requireCleanTarget) {
|
|
2903
|
+
const { stdout } = await execAsync("git status --porcelain", {
|
|
2904
|
+
cwd,
|
|
2905
|
+
env: READ_ONLY_GIT_ENV,
|
|
2906
|
+
});
|
|
2907
|
+
if (stdout.trim().length > 0) {
|
|
2908
|
+
throw new Error("Working directory has uncommitted changes.");
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
|
|
2912
|
+
if (!baseRef) {
|
|
2913
|
+
throw new Error("Base branch is required for merge");
|
|
2914
|
+
}
|
|
2915
|
+
if (baseRef.startsWith("origin/")) {
|
|
2916
|
+
baseRef = baseRef.slice("origin/".length);
|
|
2917
|
+
}
|
|
2918
|
+
await mergeToBase(cwd, {
|
|
2919
|
+
baseRef,
|
|
2920
|
+
mode: msg.strategy === "squash" ? "squash" : "merge",
|
|
2921
|
+
}, { paseoHome: this.paseoHome });
|
|
2922
|
+
this.scheduleCheckoutDiffRefreshForCwd(cwd);
|
|
2923
|
+
this.emit({
|
|
2924
|
+
type: "checkout_merge_response",
|
|
2925
|
+
payload: {
|
|
2926
|
+
cwd,
|
|
2927
|
+
success: true,
|
|
2928
|
+
error: null,
|
|
2929
|
+
requestId,
|
|
2930
|
+
},
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
catch (error) {
|
|
2934
|
+
this.emit({
|
|
2935
|
+
type: "checkout_merge_response",
|
|
2936
|
+
payload: {
|
|
2937
|
+
cwd,
|
|
2938
|
+
success: false,
|
|
2939
|
+
error: this.toCheckoutError(error),
|
|
2940
|
+
requestId,
|
|
2941
|
+
},
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
async handleCheckoutMergeFromBaseRequest(msg) {
|
|
2946
|
+
const { cwd, requestId } = msg;
|
|
2947
|
+
try {
|
|
2948
|
+
if (msg.requireCleanTarget ?? true) {
|
|
2949
|
+
const { stdout } = await execAsync("git status --porcelain", {
|
|
2950
|
+
cwd,
|
|
2951
|
+
env: READ_ONLY_GIT_ENV,
|
|
2952
|
+
});
|
|
2953
|
+
if (stdout.trim().length > 0) {
|
|
2954
|
+
throw new Error("Working directory has uncommitted changes.");
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
await mergeFromBase(cwd, {
|
|
2958
|
+
baseRef: msg.baseRef,
|
|
2959
|
+
requireCleanTarget: msg.requireCleanTarget ?? true,
|
|
2960
|
+
});
|
|
2961
|
+
this.scheduleCheckoutDiffRefreshForCwd(cwd);
|
|
2962
|
+
this.emit({
|
|
2963
|
+
type: "checkout_merge_from_base_response",
|
|
2964
|
+
payload: {
|
|
2965
|
+
cwd,
|
|
2966
|
+
success: true,
|
|
2967
|
+
error: null,
|
|
2968
|
+
requestId,
|
|
2969
|
+
},
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
catch (error) {
|
|
2973
|
+
this.emit({
|
|
2974
|
+
type: "checkout_merge_from_base_response",
|
|
2975
|
+
payload: {
|
|
2976
|
+
cwd,
|
|
2977
|
+
success: false,
|
|
2978
|
+
error: this.toCheckoutError(error),
|
|
2979
|
+
requestId,
|
|
2980
|
+
},
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
async handleCheckoutPushRequest(msg) {
|
|
2985
|
+
const { cwd, requestId } = msg;
|
|
2986
|
+
try {
|
|
2987
|
+
await pushCurrentBranch(cwd);
|
|
2988
|
+
this.emit({
|
|
2989
|
+
type: "checkout_push_response",
|
|
2990
|
+
payload: {
|
|
2991
|
+
cwd,
|
|
2992
|
+
success: true,
|
|
2993
|
+
error: null,
|
|
2994
|
+
requestId,
|
|
2995
|
+
},
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
catch (error) {
|
|
2999
|
+
this.emit({
|
|
3000
|
+
type: "checkout_push_response",
|
|
3001
|
+
payload: {
|
|
3002
|
+
cwd,
|
|
3003
|
+
success: false,
|
|
3004
|
+
error: this.toCheckoutError(error),
|
|
3005
|
+
requestId,
|
|
3006
|
+
},
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
async handleCheckoutPrCreateRequest(msg) {
|
|
3011
|
+
const { cwd, requestId } = msg;
|
|
3012
|
+
try {
|
|
3013
|
+
let title = msg.title?.trim() ?? "";
|
|
3014
|
+
let body = msg.body?.trim() ?? "";
|
|
3015
|
+
if (!title || !body) {
|
|
3016
|
+
const generated = await this.generatePullRequestText(cwd, msg.baseRef);
|
|
3017
|
+
if (!title)
|
|
3018
|
+
title = generated.title;
|
|
3019
|
+
if (!body)
|
|
3020
|
+
body = generated.body;
|
|
3021
|
+
}
|
|
3022
|
+
const result = await createPullRequest(cwd, {
|
|
3023
|
+
title,
|
|
3024
|
+
body,
|
|
3025
|
+
base: msg.baseRef,
|
|
3026
|
+
});
|
|
3027
|
+
this.emit({
|
|
3028
|
+
type: "checkout_pr_create_response",
|
|
3029
|
+
payload: {
|
|
3030
|
+
cwd,
|
|
3031
|
+
url: result.url ?? null,
|
|
3032
|
+
number: result.number ?? null,
|
|
3033
|
+
error: null,
|
|
3034
|
+
requestId,
|
|
3035
|
+
},
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
catch (error) {
|
|
3039
|
+
this.emit({
|
|
3040
|
+
type: "checkout_pr_create_response",
|
|
3041
|
+
payload: {
|
|
3042
|
+
cwd,
|
|
3043
|
+
url: null,
|
|
3044
|
+
number: null,
|
|
3045
|
+
error: this.toCheckoutError(error),
|
|
3046
|
+
requestId,
|
|
3047
|
+
},
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
async handleCheckoutPrStatusRequest(msg) {
|
|
3052
|
+
const { cwd, requestId } = msg;
|
|
3053
|
+
try {
|
|
3054
|
+
const status = await getPullRequestStatus(cwd);
|
|
3055
|
+
this.emit({
|
|
3056
|
+
type: "checkout_pr_status_response",
|
|
3057
|
+
payload: {
|
|
3058
|
+
cwd,
|
|
3059
|
+
status,
|
|
3060
|
+
error: null,
|
|
3061
|
+
requestId,
|
|
3062
|
+
},
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
catch (error) {
|
|
3066
|
+
this.emit({
|
|
3067
|
+
type: "checkout_pr_status_response",
|
|
3068
|
+
payload: {
|
|
3069
|
+
cwd,
|
|
3070
|
+
status: null,
|
|
3071
|
+
error: this.toCheckoutError(error),
|
|
3072
|
+
requestId,
|
|
3073
|
+
},
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
async handlePaseoWorktreeListRequest(msg) {
|
|
3078
|
+
const { requestId } = msg;
|
|
3079
|
+
const cwd = msg.repoRoot ?? msg.cwd;
|
|
3080
|
+
if (!cwd) {
|
|
3081
|
+
this.emit({
|
|
3082
|
+
type: "paseo_worktree_list_response",
|
|
3083
|
+
payload: {
|
|
3084
|
+
worktrees: [],
|
|
3085
|
+
error: { code: "UNKNOWN", message: "cwd or repoRoot is required" },
|
|
3086
|
+
requestId,
|
|
3087
|
+
},
|
|
3088
|
+
});
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
try {
|
|
3092
|
+
const worktrees = await listPaseoWorktrees({ cwd, paseoHome: this.paseoHome });
|
|
3093
|
+
this.emit({
|
|
3094
|
+
type: "paseo_worktree_list_response",
|
|
3095
|
+
payload: {
|
|
3096
|
+
worktrees: worktrees.map((entry) => ({
|
|
3097
|
+
worktreePath: entry.path,
|
|
3098
|
+
branchName: entry.branchName ?? null,
|
|
3099
|
+
head: entry.head ?? null,
|
|
3100
|
+
})),
|
|
3101
|
+
error: null,
|
|
3102
|
+
requestId,
|
|
3103
|
+
},
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
catch (error) {
|
|
3107
|
+
this.emit({
|
|
3108
|
+
type: "paseo_worktree_list_response",
|
|
3109
|
+
payload: {
|
|
3110
|
+
worktrees: [],
|
|
3111
|
+
error: this.toCheckoutError(error),
|
|
3112
|
+
requestId,
|
|
3113
|
+
},
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
async handlePaseoWorktreeArchiveRequest(msg) {
|
|
3118
|
+
const { requestId } = msg;
|
|
3119
|
+
let targetPath = msg.worktreePath;
|
|
3120
|
+
let repoRoot = msg.repoRoot ?? null;
|
|
3121
|
+
try {
|
|
3122
|
+
if (!targetPath) {
|
|
3123
|
+
if (!repoRoot || !msg.branchName) {
|
|
3124
|
+
throw new Error("worktreePath or repoRoot+branchName is required");
|
|
3125
|
+
}
|
|
3126
|
+
const worktrees = await listPaseoWorktrees({ cwd: repoRoot, paseoHome: this.paseoHome });
|
|
3127
|
+
const match = worktrees.find((entry) => entry.branchName === msg.branchName);
|
|
3128
|
+
if (!match) {
|
|
3129
|
+
throw new Error(`Paseo worktree not found for branch ${msg.branchName}`);
|
|
3130
|
+
}
|
|
3131
|
+
targetPath = match.path;
|
|
3132
|
+
}
|
|
3133
|
+
const ownership = await isPaseoOwnedWorktreeCwd(targetPath, { paseoHome: this.paseoHome });
|
|
3134
|
+
if (!ownership.allowed) {
|
|
3135
|
+
this.emit({
|
|
3136
|
+
type: "paseo_worktree_archive_response",
|
|
3137
|
+
payload: {
|
|
3138
|
+
success: false,
|
|
3139
|
+
removedAgents: [],
|
|
3140
|
+
error: {
|
|
3141
|
+
code: "NOT_ALLOWED",
|
|
3142
|
+
message: "Worktree is not a Paseo-owned worktree",
|
|
3143
|
+
},
|
|
3144
|
+
requestId,
|
|
3145
|
+
},
|
|
3146
|
+
});
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
repoRoot = ownership.repoRoot ?? repoRoot ?? null;
|
|
3150
|
+
if (!repoRoot) {
|
|
3151
|
+
throw new Error("Unable to resolve repo root for worktree");
|
|
3152
|
+
}
|
|
3153
|
+
const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
|
|
3154
|
+
paseoHome: this.paseoHome,
|
|
3155
|
+
});
|
|
3156
|
+
if (resolvedWorktree) {
|
|
3157
|
+
targetPath = resolvedWorktree.worktreePath;
|
|
3158
|
+
}
|
|
3159
|
+
const removedAgents = new Set();
|
|
3160
|
+
const agents = this.agentManager.listAgents();
|
|
3161
|
+
for (const agent of agents) {
|
|
3162
|
+
if (this.isPathWithinRoot(targetPath, agent.cwd)) {
|
|
3163
|
+
removedAgents.add(agent.id);
|
|
3164
|
+
try {
|
|
3165
|
+
await this.agentManager.closeAgent(agent.id);
|
|
3166
|
+
}
|
|
3167
|
+
catch {
|
|
3168
|
+
// ignore cleanup errors
|
|
3169
|
+
}
|
|
3170
|
+
try {
|
|
3171
|
+
await this.agentStorage.remove(agent.id);
|
|
3172
|
+
}
|
|
3173
|
+
catch {
|
|
3174
|
+
// ignore cleanup errors
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
const registryRecords = await this.agentStorage.list();
|
|
3179
|
+
for (const record of registryRecords) {
|
|
3180
|
+
if (this.isPathWithinRoot(targetPath, record.cwd)) {
|
|
3181
|
+
removedAgents.add(record.id);
|
|
3182
|
+
try {
|
|
3183
|
+
await this.agentStorage.remove(record.id);
|
|
3184
|
+
}
|
|
3185
|
+
catch {
|
|
3186
|
+
// ignore cleanup errors
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
await deletePaseoWorktree({
|
|
3191
|
+
cwd: repoRoot,
|
|
3192
|
+
worktreePath: targetPath,
|
|
3193
|
+
paseoHome: this.paseoHome,
|
|
3194
|
+
});
|
|
3195
|
+
for (const agentId of removedAgents) {
|
|
3196
|
+
this.emit({
|
|
3197
|
+
type: "agent_deleted",
|
|
3198
|
+
payload: {
|
|
3199
|
+
agentId,
|
|
3200
|
+
requestId,
|
|
3201
|
+
},
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
this.emit({
|
|
3205
|
+
type: "paseo_worktree_archive_response",
|
|
3206
|
+
payload: {
|
|
3207
|
+
success: true,
|
|
3208
|
+
removedAgents: Array.from(removedAgents),
|
|
3209
|
+
error: null,
|
|
3210
|
+
requestId,
|
|
3211
|
+
},
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
catch (error) {
|
|
3215
|
+
this.emit({
|
|
3216
|
+
type: "paseo_worktree_archive_response",
|
|
3217
|
+
payload: {
|
|
3218
|
+
success: false,
|
|
3219
|
+
removedAgents: [],
|
|
3220
|
+
error: this.toCheckoutError(error),
|
|
3221
|
+
requestId,
|
|
3222
|
+
},
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
/**
|
|
3227
|
+
* Handle highlighted diff request - returns parsed and syntax-highlighted diff
|
|
3228
|
+
*/
|
|
3229
|
+
async handleHighlightedDiffRequest(agentId, requestId) {
|
|
3230
|
+
this.sessionLogger.debug({ agentId }, `Handling highlighted diff request for agent ${agentId}`);
|
|
3231
|
+
// Maximum lines changed before we skip showing the diff content
|
|
3232
|
+
const MAX_DIFF_LINES = 5000;
|
|
3233
|
+
try {
|
|
3234
|
+
const agents = this.agentManager.listAgents();
|
|
3235
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
3236
|
+
if (!agent) {
|
|
3237
|
+
this.emit({
|
|
3238
|
+
type: "highlighted_diff_response",
|
|
3239
|
+
payload: {
|
|
3240
|
+
agentId,
|
|
3241
|
+
files: [],
|
|
3242
|
+
error: `Agent not found: ${agentId}`,
|
|
3243
|
+
requestId,
|
|
3244
|
+
},
|
|
3245
|
+
});
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
// Step 1: Get the list of changed files with their stats (numstat gives additions/deletions per file)
|
|
3249
|
+
const { stdout: numstatOutput } = await execAsync("git diff --numstat HEAD", { cwd: agent.cwd });
|
|
3250
|
+
// Get file statuses (A=added, D=deleted, M=modified) to detect deleted files
|
|
3251
|
+
const { stdout: nameStatusOutput } = await execAsync("git diff --name-status HEAD", { cwd: agent.cwd });
|
|
3252
|
+
const deletedFiles = new Set();
|
|
3253
|
+
const addedFiles = new Set();
|
|
3254
|
+
for (const line of nameStatusOutput.trim().split("\n").filter(Boolean)) {
|
|
3255
|
+
const [status, ...pathParts] = line.split("\t");
|
|
3256
|
+
const path = pathParts.join("\t");
|
|
3257
|
+
if (status === "D") {
|
|
3258
|
+
deletedFiles.add(path);
|
|
3259
|
+
}
|
|
3260
|
+
else if (status === "A") {
|
|
3261
|
+
addedFiles.add(path);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
const fileStats = [];
|
|
3265
|
+
for (const line of numstatOutput.trim().split("\n").filter(Boolean)) {
|
|
3266
|
+
const parts = line.split("\t");
|
|
3267
|
+
if (parts.length >= 3) {
|
|
3268
|
+
const [addStr, delStr, ...pathParts] = parts;
|
|
3269
|
+
const path = pathParts.join("\t"); // Handle paths with tabs
|
|
3270
|
+
const isBinary = addStr === "-" && delStr === "-";
|
|
3271
|
+
fileStats.push({
|
|
3272
|
+
path,
|
|
3273
|
+
additions: isBinary ? 0 : parseInt(addStr, 10),
|
|
3274
|
+
deletions: isBinary ? 0 : parseInt(delStr, 10),
|
|
3275
|
+
isBinary,
|
|
3276
|
+
isTracked: true,
|
|
3277
|
+
isDeleted: deletedFiles.has(path),
|
|
3278
|
+
isNew: addedFiles.has(path),
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
// Step 2: Get untracked files
|
|
3283
|
+
try {
|
|
3284
|
+
const { stdout: untrackedFiles } = await execAsync("git ls-files --others --exclude-standard", { cwd: agent.cwd });
|
|
3285
|
+
for (const filePath of untrackedFiles.trim().split("\n").filter(Boolean)) {
|
|
3286
|
+
// Use git's numstat with --no-index to detect binary files (cross-platform)
|
|
3287
|
+
// Binary files show as "-\t-\tfilepath", text files show line counts
|
|
3288
|
+
try {
|
|
3289
|
+
const { stdout: numstatLine } = await execAsync(`git diff --numstat --no-index /dev/null "${filePath}" || true`, { cwd: agent.cwd });
|
|
3290
|
+
const parts = numstatLine.trim().split("\t");
|
|
3291
|
+
const isBinary = parts[0] === "-" && parts[1] === "-";
|
|
3292
|
+
const additions = isBinary ? 0 : (parseInt(parts[0], 10) || 0);
|
|
3293
|
+
fileStats.push({
|
|
3294
|
+
path: filePath,
|
|
3295
|
+
additions,
|
|
3296
|
+
deletions: 0,
|
|
3297
|
+
isBinary,
|
|
3298
|
+
isTracked: false,
|
|
3299
|
+
isDeleted: false,
|
|
3300
|
+
isNew: true,
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
catch {
|
|
3304
|
+
// If we can't determine, assume text and try to get it
|
|
3305
|
+
fileStats.push({
|
|
3306
|
+
path: filePath,
|
|
3307
|
+
additions: 0,
|
|
3308
|
+
deletions: 0,
|
|
3309
|
+
isBinary: false,
|
|
3310
|
+
isTracked: false,
|
|
3311
|
+
isDeleted: false,
|
|
3312
|
+
isNew: true,
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
catch {
|
|
3318
|
+
// Ignore errors getting untracked files
|
|
3319
|
+
}
|
|
3320
|
+
// Step 3: Fetch diffs per-file, respecting limits
|
|
3321
|
+
const allFiles = [];
|
|
3322
|
+
for (const stats of fileStats) {
|
|
3323
|
+
const totalLines = stats.additions + stats.deletions;
|
|
3324
|
+
// Handle binary files
|
|
3325
|
+
if (stats.isBinary) {
|
|
3326
|
+
allFiles.push({
|
|
3327
|
+
path: stats.path,
|
|
3328
|
+
isNew: stats.isNew,
|
|
3329
|
+
isDeleted: stats.isDeleted,
|
|
3330
|
+
additions: 0,
|
|
3331
|
+
deletions: 0,
|
|
3332
|
+
hunks: [],
|
|
3333
|
+
status: "binary",
|
|
3334
|
+
});
|
|
3335
|
+
continue;
|
|
3336
|
+
}
|
|
3337
|
+
// Handle files that are too large
|
|
3338
|
+
if (totalLines > MAX_DIFF_LINES) {
|
|
3339
|
+
allFiles.push({
|
|
3340
|
+
path: stats.path,
|
|
3341
|
+
isNew: stats.isNew,
|
|
3342
|
+
isDeleted: stats.isDeleted,
|
|
3343
|
+
additions: stats.additions,
|
|
3344
|
+
deletions: stats.deletions,
|
|
3345
|
+
hunks: [],
|
|
3346
|
+
status: "too_large",
|
|
3347
|
+
});
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
// Fetch the actual diff for this file
|
|
3351
|
+
try {
|
|
3352
|
+
let fileDiff;
|
|
3353
|
+
if (stats.isTracked) {
|
|
3354
|
+
const { stdout } = await execAsync(`git diff HEAD -- "${stats.path}"`, { cwd: agent.cwd });
|
|
3355
|
+
fileDiff = stdout;
|
|
3356
|
+
}
|
|
3357
|
+
else {
|
|
3358
|
+
const { stdout } = await execAsync(`git diff --no-index /dev/null "${stats.path}" || true`, { cwd: agent.cwd });
|
|
3359
|
+
fileDiff = stdout;
|
|
3360
|
+
}
|
|
3361
|
+
if (fileDiff) {
|
|
3362
|
+
const parsedFiles = await parseAndHighlightDiff(fileDiff, agent.cwd);
|
|
3363
|
+
for (const file of parsedFiles) {
|
|
3364
|
+
allFiles.push({ ...file, status: "ok" });
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
catch {
|
|
3369
|
+
// If diff fails for this file, add it with empty hunks
|
|
3370
|
+
allFiles.push({
|
|
3371
|
+
path: stats.path,
|
|
3372
|
+
isNew: stats.isNew,
|
|
3373
|
+
isDeleted: stats.isDeleted,
|
|
3374
|
+
additions: stats.additions,
|
|
3375
|
+
deletions: stats.deletions,
|
|
3376
|
+
hunks: [],
|
|
3377
|
+
status: "ok",
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
this.emit({
|
|
3382
|
+
type: "highlighted_diff_response",
|
|
3383
|
+
payload: {
|
|
3384
|
+
agentId,
|
|
3385
|
+
files: allFiles,
|
|
3386
|
+
error: null,
|
|
3387
|
+
requestId,
|
|
3388
|
+
},
|
|
3389
|
+
});
|
|
3390
|
+
this.sessionLogger.debug({ agentId, fileCount: allFiles.length }, `Highlighted diff for agent ${agentId} completed (${allFiles.length} files)`);
|
|
3391
|
+
}
|
|
3392
|
+
catch (error) {
|
|
3393
|
+
this.sessionLogger.error({ err: error, agentId }, `Failed to get highlighted diff for agent ${agentId}`);
|
|
3394
|
+
this.emit({
|
|
3395
|
+
type: "highlighted_diff_response",
|
|
3396
|
+
payload: {
|
|
3397
|
+
agentId,
|
|
3398
|
+
files: [],
|
|
3399
|
+
error: error.message,
|
|
3400
|
+
requestId,
|
|
3401
|
+
},
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Handle read-only file explorer requests scoped to an agent's cwd
|
|
3407
|
+
*/
|
|
3408
|
+
async handleFileExplorerRequest(request) {
|
|
3409
|
+
const { agentId, path: requestedPath = ".", mode, requestId } = request;
|
|
3410
|
+
this.sessionLogger.debug({ agentId, mode, path: requestedPath }, `Handling file explorer request for agent ${agentId} (${mode} ${requestedPath})`);
|
|
3411
|
+
try {
|
|
3412
|
+
const agents = this.agentManager.listAgents();
|
|
3413
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
3414
|
+
if (!agent) {
|
|
3415
|
+
this.emit({
|
|
3416
|
+
type: "file_explorer_response",
|
|
3417
|
+
payload: {
|
|
3418
|
+
agentId,
|
|
3419
|
+
path: requestedPath,
|
|
3420
|
+
mode,
|
|
3421
|
+
directory: null,
|
|
3422
|
+
file: null,
|
|
3423
|
+
error: `Agent not found: ${agentId}`,
|
|
3424
|
+
requestId,
|
|
3425
|
+
},
|
|
3426
|
+
});
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
if (mode === "list") {
|
|
3430
|
+
const directory = await listDirectoryEntries({
|
|
3431
|
+
root: agent.cwd,
|
|
3432
|
+
relativePath: requestedPath,
|
|
3433
|
+
});
|
|
3434
|
+
this.emit({
|
|
3435
|
+
type: "file_explorer_response",
|
|
3436
|
+
payload: {
|
|
3437
|
+
agentId,
|
|
3438
|
+
path: directory.path,
|
|
3439
|
+
mode,
|
|
3440
|
+
directory,
|
|
3441
|
+
file: null,
|
|
3442
|
+
error: null,
|
|
3443
|
+
requestId,
|
|
3444
|
+
},
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
else {
|
|
3448
|
+
const file = await readExplorerFile({
|
|
3449
|
+
root: agent.cwd,
|
|
3450
|
+
relativePath: requestedPath,
|
|
3451
|
+
});
|
|
3452
|
+
this.emit({
|
|
3453
|
+
type: "file_explorer_response",
|
|
3454
|
+
payload: {
|
|
3455
|
+
agentId,
|
|
3456
|
+
path: file.path,
|
|
3457
|
+
mode,
|
|
3458
|
+
directory: null,
|
|
3459
|
+
file,
|
|
3460
|
+
error: null,
|
|
3461
|
+
requestId,
|
|
3462
|
+
},
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
catch (error) {
|
|
3467
|
+
this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to fulfill file explorer request for agent ${agentId}`);
|
|
3468
|
+
this.emit({
|
|
3469
|
+
type: "file_explorer_response",
|
|
3470
|
+
payload: {
|
|
3471
|
+
agentId,
|
|
3472
|
+
path: requestedPath,
|
|
3473
|
+
mode,
|
|
3474
|
+
directory: null,
|
|
3475
|
+
file: null,
|
|
3476
|
+
error: error.message,
|
|
3477
|
+
requestId,
|
|
3478
|
+
},
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Handle project icon request for a given cwd
|
|
3484
|
+
*/
|
|
3485
|
+
async handleProjectIconRequest(request) {
|
|
3486
|
+
const { cwd, requestId } = request;
|
|
3487
|
+
try {
|
|
3488
|
+
const icon = await getProjectIcon(cwd);
|
|
3489
|
+
this.emit({
|
|
3490
|
+
type: "project_icon_response",
|
|
3491
|
+
payload: {
|
|
3492
|
+
cwd,
|
|
3493
|
+
icon,
|
|
3494
|
+
error: null,
|
|
3495
|
+
requestId,
|
|
3496
|
+
},
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
catch (error) {
|
|
3500
|
+
this.emit({
|
|
3501
|
+
type: "project_icon_response",
|
|
3502
|
+
payload: {
|
|
3503
|
+
cwd,
|
|
3504
|
+
icon: null,
|
|
3505
|
+
error: error.message,
|
|
3506
|
+
requestId,
|
|
3507
|
+
},
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Handle file download token request scoped to an agent's cwd
|
|
3513
|
+
*/
|
|
3514
|
+
async handleFileDownloadTokenRequest(request) {
|
|
3515
|
+
const { agentId, path: requestedPath, requestId } = request;
|
|
3516
|
+
this.sessionLogger.debug({ agentId, path: requestedPath }, `Handling file download token request for agent ${agentId} (${requestedPath})`);
|
|
3517
|
+
try {
|
|
3518
|
+
const agents = this.agentManager.listAgents();
|
|
3519
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
3520
|
+
if (!agent) {
|
|
3521
|
+
this.emit({
|
|
3522
|
+
type: "file_download_token_response",
|
|
3523
|
+
payload: {
|
|
3524
|
+
agentId,
|
|
3525
|
+
path: requestedPath,
|
|
3526
|
+
token: null,
|
|
3527
|
+
fileName: null,
|
|
3528
|
+
mimeType: null,
|
|
3529
|
+
size: null,
|
|
3530
|
+
error: `Agent not found: ${agentId}`,
|
|
3531
|
+
requestId,
|
|
3532
|
+
},
|
|
3533
|
+
});
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
const info = await getDownloadableFileInfo({
|
|
3537
|
+
root: agent.cwd,
|
|
3538
|
+
relativePath: requestedPath,
|
|
3539
|
+
});
|
|
3540
|
+
const entry = this.downloadTokenStore.issueToken({
|
|
3541
|
+
agentId,
|
|
3542
|
+
path: info.path,
|
|
3543
|
+
absolutePath: info.absolutePath,
|
|
3544
|
+
fileName: info.fileName,
|
|
3545
|
+
mimeType: info.mimeType,
|
|
3546
|
+
size: info.size,
|
|
3547
|
+
});
|
|
3548
|
+
this.emit({
|
|
3549
|
+
type: "file_download_token_response",
|
|
3550
|
+
payload: {
|
|
3551
|
+
agentId,
|
|
3552
|
+
path: info.path,
|
|
3553
|
+
token: entry.token,
|
|
3554
|
+
fileName: entry.fileName,
|
|
3555
|
+
mimeType: entry.mimeType,
|
|
3556
|
+
size: entry.size,
|
|
3557
|
+
error: null,
|
|
3558
|
+
requestId,
|
|
3559
|
+
},
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
catch (error) {
|
|
3563
|
+
this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to issue download token for agent ${agentId}`);
|
|
3564
|
+
this.emit({
|
|
3565
|
+
type: "file_download_token_response",
|
|
3566
|
+
payload: {
|
|
3567
|
+
agentId,
|
|
3568
|
+
path: requestedPath,
|
|
3569
|
+
token: null,
|
|
3570
|
+
fileName: null,
|
|
3571
|
+
mimeType: null,
|
|
3572
|
+
size: null,
|
|
3573
|
+
error: error.message,
|
|
3574
|
+
requestId,
|
|
3575
|
+
},
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
/**
|
|
3580
|
+
* Build the current agent list payload (live + persisted), optionally filtered by labels.
|
|
3581
|
+
*/
|
|
3582
|
+
async listAgentPayloads(filter) {
|
|
3583
|
+
// Get live agents with session modes
|
|
3584
|
+
const agentSnapshots = this.agentManager.listAgents();
|
|
3585
|
+
const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
|
|
3586
|
+
// Add persisted agents that have not been lazily initialized yet
|
|
3587
|
+
// (excluding internal agents which are for ephemeral system tasks)
|
|
3588
|
+
const registryRecords = await this.agentStorage.list();
|
|
3589
|
+
const liveIds = new Set(agentSnapshots.map((a) => a.id));
|
|
3590
|
+
const persistedAgents = registryRecords
|
|
3591
|
+
.filter((record) => !liveIds.has(record.id) && !record.internal)
|
|
3592
|
+
.map((record) => this.buildStoredAgentPayload(record));
|
|
3593
|
+
let agents = [...liveAgents, ...persistedAgents];
|
|
3594
|
+
// Filter by labels if filter provided
|
|
3595
|
+
if (filter?.labels) {
|
|
3596
|
+
const filterLabels = filter.labels;
|
|
3597
|
+
agents = agents.filter((agent) => Object.entries(filterLabels).every(([key, value]) => agent.labels[key] === value));
|
|
3598
|
+
}
|
|
3599
|
+
return agents;
|
|
3600
|
+
}
|
|
3601
|
+
async resolveAgentIdentifier(identifier) {
|
|
3602
|
+
const trimmed = identifier.trim();
|
|
3603
|
+
if (!trimmed) {
|
|
3604
|
+
return { ok: false, error: "Agent identifier cannot be empty" };
|
|
3605
|
+
}
|
|
3606
|
+
const stored = await this.agentStorage.list();
|
|
3607
|
+
const storedRecords = stored.filter((record) => !record.internal);
|
|
3608
|
+
const knownIds = new Set();
|
|
3609
|
+
for (const record of storedRecords) {
|
|
3610
|
+
knownIds.add(record.id);
|
|
3611
|
+
}
|
|
3612
|
+
for (const agent of this.agentManager.listAgents()) {
|
|
3613
|
+
knownIds.add(agent.id);
|
|
3614
|
+
}
|
|
3615
|
+
if (knownIds.has(trimmed)) {
|
|
3616
|
+
return { ok: true, agentId: trimmed };
|
|
3617
|
+
}
|
|
3618
|
+
const prefixMatches = Array.from(knownIds).filter((id) => id.startsWith(trimmed));
|
|
3619
|
+
if (prefixMatches.length === 1) {
|
|
3620
|
+
return { ok: true, agentId: prefixMatches[0] };
|
|
3621
|
+
}
|
|
3622
|
+
if (prefixMatches.length > 1) {
|
|
3623
|
+
return {
|
|
3624
|
+
ok: false,
|
|
3625
|
+
error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
|
|
3626
|
+
.slice(0, 5)
|
|
3627
|
+
.map((id) => id.slice(0, 8))
|
|
3628
|
+
.join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
const titleMatches = storedRecords.filter((record) => record.title === trimmed);
|
|
3632
|
+
if (titleMatches.length === 1) {
|
|
3633
|
+
return { ok: true, agentId: titleMatches[0].id };
|
|
3634
|
+
}
|
|
3635
|
+
if (titleMatches.length > 1) {
|
|
3636
|
+
return {
|
|
3637
|
+
ok: false,
|
|
3638
|
+
error: `Agent title "${trimmed}" is ambiguous (${titleMatches
|
|
3639
|
+
.slice(0, 5)
|
|
3640
|
+
.map((r) => r.id.slice(0, 8))
|
|
3641
|
+
.join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
return { ok: false, error: `Agent not found: ${trimmed}` };
|
|
3645
|
+
}
|
|
3646
|
+
async getAgentPayloadById(agentId) {
|
|
3647
|
+
const live = this.agentManager.getAgent(agentId);
|
|
3648
|
+
if (live) {
|
|
3649
|
+
return await this.buildAgentPayload(live);
|
|
3650
|
+
}
|
|
3651
|
+
const record = await this.agentStorage.get(agentId);
|
|
3652
|
+
if (!record || record.internal) {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3655
|
+
return this.buildStoredAgentPayload(record);
|
|
3656
|
+
}
|
|
3657
|
+
async handleFetchAgents(requestId, filter) {
|
|
3658
|
+
try {
|
|
3659
|
+
const agents = await this.listAgentPayloads(filter);
|
|
3660
|
+
this.emit({
|
|
3661
|
+
type: "fetch_agents_response",
|
|
3662
|
+
payload: { requestId, agents },
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
catch (error) {
|
|
3666
|
+
this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
|
|
3667
|
+
this.emit({
|
|
3668
|
+
type: "fetch_agents_response",
|
|
3669
|
+
payload: { requestId, agents: [] },
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
async listAgentsGroupedByProjectPayload(filter) {
|
|
3674
|
+
const agents = await this.listAgentPayloads(filter);
|
|
3675
|
+
const visibleAgents = agents
|
|
3676
|
+
.filter((agent) => !agent.archivedAt)
|
|
3677
|
+
.sort((left, right) => Date.parse(right.updatedAt || "") - Date.parse(left.updatedAt || ""));
|
|
3678
|
+
const grouped = new Map();
|
|
3679
|
+
// Warm project placement status for all visible roots up front to avoid serial N+1 latency.
|
|
3680
|
+
for (const agent of visibleAgents) {
|
|
3681
|
+
void this.getProjectPlacement(agent.cwd);
|
|
3682
|
+
}
|
|
3683
|
+
for (const agent of visibleAgents) {
|
|
3684
|
+
const project = await this.getProjectPlacement(agent.cwd);
|
|
3685
|
+
const projectKey = project.projectKey;
|
|
3686
|
+
let group = grouped.get(projectKey);
|
|
3687
|
+
if (!group) {
|
|
3688
|
+
group = {
|
|
3689
|
+
projectKey,
|
|
3690
|
+
projectName: project.projectName,
|
|
3691
|
+
agents: [],
|
|
3692
|
+
};
|
|
3693
|
+
grouped.set(projectKey, group);
|
|
3694
|
+
}
|
|
3695
|
+
if (group.agents.length >= MAX_AGENTS_PER_PROJECT) {
|
|
3696
|
+
continue;
|
|
3697
|
+
}
|
|
3698
|
+
group.agents.push({ agent, checkout: project.checkout });
|
|
3699
|
+
}
|
|
3700
|
+
return Array.from(grouped.values());
|
|
3701
|
+
}
|
|
3702
|
+
async handleFetchAgentsGroupedByProject(requestId, filter) {
|
|
3703
|
+
try {
|
|
3704
|
+
const groups = await this.listAgentsGroupedByProjectPayload(filter);
|
|
3705
|
+
this.emit({
|
|
3706
|
+
type: "fetch_agents_grouped_by_project_response",
|
|
3707
|
+
payload: { requestId, groups },
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
catch (error) {
|
|
3711
|
+
this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_grouped_by_project_request");
|
|
3712
|
+
this.emit({
|
|
3713
|
+
type: "fetch_agents_grouped_by_project_response",
|
|
3714
|
+
payload: { requestId, groups: [] },
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
async handleFetchAgent(agentIdOrIdentifier, requestId) {
|
|
3719
|
+
const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
|
|
3720
|
+
if (!resolved.ok) {
|
|
3721
|
+
this.emit({
|
|
3722
|
+
type: "fetch_agent_response",
|
|
3723
|
+
payload: { requestId, agent: null, error: resolved.error },
|
|
3724
|
+
});
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const agent = await this.getAgentPayloadById(resolved.agentId);
|
|
3728
|
+
if (!agent) {
|
|
3729
|
+
this.emit({
|
|
3730
|
+
type: "fetch_agent_response",
|
|
3731
|
+
payload: { requestId, agent: null, error: `Agent not found: ${resolved.agentId}` },
|
|
3732
|
+
});
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
this.emit({
|
|
3736
|
+
type: "fetch_agent_response",
|
|
3737
|
+
payload: { requestId, agent, error: null },
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
async handleSendAgentMessageRequest(msg) {
|
|
3741
|
+
const resolved = await this.resolveAgentIdentifier(msg.agentId);
|
|
3742
|
+
if (!resolved.ok) {
|
|
3743
|
+
this.emit({
|
|
3744
|
+
type: "send_agent_message_response",
|
|
3745
|
+
payload: {
|
|
3746
|
+
requestId: msg.requestId,
|
|
3747
|
+
agentId: msg.agentId,
|
|
3748
|
+
accepted: false,
|
|
3749
|
+
error: resolved.error,
|
|
3750
|
+
},
|
|
3751
|
+
});
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
try {
|
|
3755
|
+
const agentId = resolved.agentId;
|
|
3756
|
+
await this.ensureAgentLoaded(agentId);
|
|
3757
|
+
await this.interruptAgentIfRunning(agentId);
|
|
3758
|
+
try {
|
|
3759
|
+
this.agentManager.recordUserMessage(agentId, msg.text, { messageId: msg.messageId });
|
|
3760
|
+
}
|
|
3761
|
+
catch (error) {
|
|
3762
|
+
this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
|
|
3763
|
+
}
|
|
3764
|
+
const prompt = this.buildAgentPrompt(msg.text, msg.images);
|
|
3765
|
+
const started = this.startAgentStream(agentId, prompt);
|
|
3766
|
+
if (!started.ok) {
|
|
3767
|
+
this.emit({
|
|
3768
|
+
type: "send_agent_message_response",
|
|
3769
|
+
payload: {
|
|
3770
|
+
requestId: msg.requestId,
|
|
3771
|
+
agentId,
|
|
3772
|
+
accepted: false,
|
|
3773
|
+
error: started.error,
|
|
3774
|
+
},
|
|
3775
|
+
});
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
const startAbort = new AbortController();
|
|
3779
|
+
const startTimeoutMs = 15000;
|
|
3780
|
+
const startTimeout = setTimeout(() => startAbort.abort("timeout"), startTimeoutMs);
|
|
3781
|
+
try {
|
|
3782
|
+
await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
|
|
3783
|
+
}
|
|
3784
|
+
catch (error) {
|
|
3785
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3786
|
+
this.emit({
|
|
3787
|
+
type: "send_agent_message_response",
|
|
3788
|
+
payload: {
|
|
3789
|
+
requestId: msg.requestId,
|
|
3790
|
+
agentId,
|
|
3791
|
+
accepted: false,
|
|
3792
|
+
error: message,
|
|
3793
|
+
},
|
|
3794
|
+
});
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
finally {
|
|
3798
|
+
clearTimeout(startTimeout);
|
|
3799
|
+
}
|
|
3800
|
+
this.emit({
|
|
3801
|
+
type: "send_agent_message_response",
|
|
3802
|
+
payload: {
|
|
3803
|
+
requestId: msg.requestId,
|
|
3804
|
+
agentId,
|
|
3805
|
+
accepted: true,
|
|
3806
|
+
error: null,
|
|
3807
|
+
},
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
catch (error) {
|
|
3811
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3812
|
+
this.emit({
|
|
3813
|
+
type: "send_agent_message_response",
|
|
3814
|
+
payload: {
|
|
3815
|
+
requestId: msg.requestId,
|
|
3816
|
+
agentId: resolved.agentId,
|
|
3817
|
+
accepted: false,
|
|
3818
|
+
error: message,
|
|
3819
|
+
},
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
async handleWaitForFinish(agentIdOrIdentifier, requestId, timeoutMs) {
|
|
3824
|
+
const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
|
|
3825
|
+
if (!resolved.ok) {
|
|
3826
|
+
this.emit({
|
|
3827
|
+
type: "wait_for_finish_response",
|
|
3828
|
+
payload: { requestId, status: "error", final: null, error: resolved.error },
|
|
3829
|
+
});
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
const agentId = resolved.agentId;
|
|
3833
|
+
const live = this.agentManager.getAgent(agentId);
|
|
3834
|
+
if (!live) {
|
|
3835
|
+
const record = await this.agentStorage.get(agentId);
|
|
3836
|
+
if (!record || record.internal) {
|
|
3837
|
+
this.emit({
|
|
3838
|
+
type: "wait_for_finish_response",
|
|
3839
|
+
payload: {
|
|
3840
|
+
requestId,
|
|
3841
|
+
status: "error",
|
|
3842
|
+
final: null,
|
|
3843
|
+
error: `Agent not found: ${agentId}`,
|
|
3844
|
+
},
|
|
3845
|
+
});
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
const final = this.buildStoredAgentPayload(record);
|
|
3849
|
+
const status = record.attentionReason === "permission"
|
|
3850
|
+
? "permission"
|
|
3851
|
+
: record.lastStatus === "error"
|
|
3852
|
+
? "error"
|
|
3853
|
+
: "idle";
|
|
3854
|
+
this.emit({
|
|
3855
|
+
type: "wait_for_finish_response",
|
|
3856
|
+
payload: { requestId, status, final, error: null },
|
|
3857
|
+
});
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
const abortController = new AbortController();
|
|
3861
|
+
const effectiveTimeoutMs = timeoutMs ?? 600000; // 10 minutes default
|
|
3862
|
+
const timeoutHandle = setTimeout(() => {
|
|
3863
|
+
abortController.abort("timeout");
|
|
3864
|
+
}, effectiveTimeoutMs);
|
|
3865
|
+
try {
|
|
3866
|
+
const result = await this.agentManager.waitForAgentEvent(agentId, {
|
|
3867
|
+
signal: abortController.signal,
|
|
3868
|
+
});
|
|
3869
|
+
const final = await this.getAgentPayloadById(agentId);
|
|
3870
|
+
if (!final) {
|
|
3871
|
+
throw new Error(`Agent ${agentId} disappeared while waiting`);
|
|
3872
|
+
}
|
|
3873
|
+
const status = result.permission
|
|
3874
|
+
? "permission"
|
|
3875
|
+
: result.status === "error"
|
|
3876
|
+
? "error"
|
|
3877
|
+
: "idle";
|
|
3878
|
+
this.emit({
|
|
3879
|
+
type: "wait_for_finish_response",
|
|
3880
|
+
payload: { requestId, status, final, error: null },
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
catch (error) {
|
|
3884
|
+
const isAbort = error instanceof Error &&
|
|
3885
|
+
(error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
|
|
3886
|
+
if (!isAbort) {
|
|
3887
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3888
|
+
this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
|
|
3889
|
+
const final = await this.getAgentPayloadById(agentId);
|
|
3890
|
+
this.emit({
|
|
3891
|
+
type: "wait_for_finish_response",
|
|
3892
|
+
payload: {
|
|
3893
|
+
requestId,
|
|
3894
|
+
status: "error",
|
|
3895
|
+
final,
|
|
3896
|
+
error: message,
|
|
3897
|
+
},
|
|
3898
|
+
});
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3901
|
+
const final = await this.getAgentPayloadById(agentId);
|
|
3902
|
+
if (!final) {
|
|
3903
|
+
throw new Error(`Agent ${agentId} disappeared while waiting`);
|
|
3904
|
+
}
|
|
3905
|
+
this.emit({
|
|
3906
|
+
type: "wait_for_finish_response",
|
|
3907
|
+
payload: { requestId, status: "timeout", final, error: null },
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
finally {
|
|
3911
|
+
clearTimeout(timeoutHandle);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
emitAgentTimelineSnapshot(agent) {
|
|
3915
|
+
const timeline = this.agentManager.getTimeline(agent.id);
|
|
3916
|
+
const events = timeline.flatMap((item) => {
|
|
3917
|
+
const serializedEvent = serializeAgentStreamEvent({
|
|
3918
|
+
type: "timeline",
|
|
3919
|
+
provider: agent.provider,
|
|
3920
|
+
item,
|
|
3921
|
+
});
|
|
3922
|
+
if (!serializedEvent) {
|
|
3923
|
+
return [];
|
|
3924
|
+
}
|
|
3925
|
+
return [
|
|
3926
|
+
{
|
|
3927
|
+
event: serializedEvent,
|
|
3928
|
+
timestamp: new Date().toISOString(),
|
|
3929
|
+
},
|
|
3930
|
+
];
|
|
3931
|
+
});
|
|
3932
|
+
this.emit({
|
|
3933
|
+
type: "agent_stream_snapshot",
|
|
3934
|
+
payload: { agentId: agent.id, events },
|
|
3935
|
+
});
|
|
3936
|
+
return timeline.length;
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Handle audio chunk for buffering and transcription
|
|
3940
|
+
*/
|
|
3941
|
+
async handleAudioChunk(msg) {
|
|
3942
|
+
if (!this.isVoiceMode) {
|
|
3943
|
+
this.sessionLogger.warn("Received voice_audio_chunk while voice mode is disabled; transcript will be emitted but voice assistant turn is skipped");
|
|
3944
|
+
}
|
|
3945
|
+
await this.handleVoiceSpeechStart();
|
|
3946
|
+
const chunkFormat = msg.format || "audio/wav";
|
|
3947
|
+
if (this.isVoiceMode) {
|
|
3948
|
+
await this.appendToActiveVoiceDictationStream(msg.audio, chunkFormat);
|
|
3949
|
+
if (!msg.isLast) {
|
|
3950
|
+
this.setVoiceModeInactivityTimeout();
|
|
3951
|
+
this.sessionLogger.debug("Voice mode: streaming chunk, waiting for speech end");
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
this.clearVoiceModeInactivityTimeout();
|
|
3955
|
+
this.sessionLogger.debug("Voice mode: speech ended, finalizing streaming transcription");
|
|
3956
|
+
await this.finalizeActiveVoiceDictationStream("speech ended");
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
const chunkBuffer = Buffer.from(msg.audio, "base64");
|
|
3960
|
+
const isPCMChunk = chunkFormat.toLowerCase().includes("pcm");
|
|
3961
|
+
if (!this.audioBuffer) {
|
|
3962
|
+
this.audioBuffer = {
|
|
3963
|
+
chunks: [],
|
|
3964
|
+
format: chunkFormat,
|
|
3965
|
+
isPCM: isPCMChunk,
|
|
3966
|
+
totalPCMBytes: 0,
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
// If the format changes mid-stream, flush what we have first
|
|
3970
|
+
if (this.audioBuffer.isPCM !== isPCMChunk) {
|
|
3971
|
+
this.sessionLogger.debug({ oldFormat: this.audioBuffer.isPCM ? "pcm" : this.audioBuffer.format, newFormat: chunkFormat }, `Audio format changed mid-stream, flushing current buffer`);
|
|
3972
|
+
const finalized = this.finalizeBufferedAudio();
|
|
3973
|
+
if (finalized) {
|
|
3974
|
+
await this.processCompletedAudio(finalized.audio, finalized.format);
|
|
3975
|
+
}
|
|
3976
|
+
this.audioBuffer = {
|
|
3977
|
+
chunks: [],
|
|
3978
|
+
format: chunkFormat,
|
|
3979
|
+
isPCM: isPCMChunk,
|
|
3980
|
+
totalPCMBytes: 0,
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
else if (!this.audioBuffer.isPCM) {
|
|
3984
|
+
// Keep latest format info for non-PCM blobs
|
|
3985
|
+
this.audioBuffer.format = chunkFormat;
|
|
3986
|
+
}
|
|
3987
|
+
this.audioBuffer.chunks.push(chunkBuffer);
|
|
3988
|
+
if (this.audioBuffer.isPCM) {
|
|
3989
|
+
this.audioBuffer.totalPCMBytes += chunkBuffer.length;
|
|
3990
|
+
}
|
|
3991
|
+
// In non-voice mode, use streaming threshold to process chunks
|
|
3992
|
+
const reachedStreamingThreshold = !this.isVoiceMode &&
|
|
3993
|
+
this.audioBuffer.isPCM &&
|
|
3994
|
+
this.audioBuffer.totalPCMBytes >= MIN_STREAMING_SEGMENT_BYTES;
|
|
3995
|
+
if (!msg.isLast && reachedStreamingThreshold) {
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
const bufferedState = this.audioBuffer;
|
|
3999
|
+
const finalized = this.finalizeBufferedAudio();
|
|
4000
|
+
if (!finalized) {
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
if (!msg.isLast && reachedStreamingThreshold) {
|
|
4004
|
+
this.sessionLogger.debug({ minDuration: MIN_STREAMING_SEGMENT_DURATION_MS, pcmBytes: bufferedState?.totalPCMBytes ?? 0 }, `Minimum chunk duration reached (~${MIN_STREAMING_SEGMENT_DURATION_MS}ms, ${bufferedState?.totalPCMBytes ?? 0} PCM bytes) – triggering STT`);
|
|
4005
|
+
}
|
|
4006
|
+
else {
|
|
4007
|
+
this.sessionLogger.debug({ audioBytes: finalized.audio.length, chunks: bufferedState?.chunks.length ?? 0 }, `Complete audio segment (${finalized.audio.length} bytes, ${bufferedState?.chunks.length ?? 0} chunk(s))`);
|
|
4008
|
+
}
|
|
4009
|
+
await this.processCompletedAudio(finalized.audio, finalized.format);
|
|
4010
|
+
}
|
|
4011
|
+
finalizeBufferedAudio() {
|
|
4012
|
+
if (!this.audioBuffer) {
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
const bufferState = this.audioBuffer;
|
|
4016
|
+
this.audioBuffer = null;
|
|
4017
|
+
if (bufferState.isPCM) {
|
|
4018
|
+
const pcmBuffer = Buffer.concat(bufferState.chunks);
|
|
4019
|
+
const wavBuffer = convertPCMToWavBuffer(pcmBuffer, PCM_SAMPLE_RATE, PCM_CHANNELS, PCM_BITS_PER_SAMPLE);
|
|
4020
|
+
return {
|
|
4021
|
+
audio: wavBuffer,
|
|
4022
|
+
format: "audio/wav",
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
return {
|
|
4026
|
+
audio: Buffer.concat(bufferState.chunks),
|
|
4027
|
+
format: bufferState.format,
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
async processCompletedAudio(audio, format) {
|
|
4031
|
+
const shouldBuffer = this.processingPhase === "transcribing" &&
|
|
4032
|
+
this.pendingAudioSegments.length === 0;
|
|
4033
|
+
if (shouldBuffer) {
|
|
4034
|
+
this.sessionLogger.debug({ phase: this.processingPhase }, `Buffering audio segment (phase: ${this.processingPhase})`);
|
|
4035
|
+
this.pendingAudioSegments.push({
|
|
4036
|
+
audio,
|
|
4037
|
+
format,
|
|
4038
|
+
});
|
|
4039
|
+
this.setBufferTimeout();
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
if (this.pendingAudioSegments.length > 0) {
|
|
4043
|
+
this.pendingAudioSegments.push({
|
|
4044
|
+
audio,
|
|
4045
|
+
format,
|
|
4046
|
+
});
|
|
4047
|
+
this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, `Processing ${this.pendingAudioSegments.length} buffered segments together`);
|
|
4048
|
+
const pendingSegments = [...this.pendingAudioSegments];
|
|
4049
|
+
this.pendingAudioSegments = [];
|
|
4050
|
+
this.clearBufferTimeout();
|
|
4051
|
+
const combinedAudio = Buffer.concat(pendingSegments.map((segment) => segment.audio));
|
|
4052
|
+
const combinedFormat = pendingSegments[pendingSegments.length - 1].format;
|
|
4053
|
+
await this.processAudio(combinedAudio, combinedFormat);
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
await this.processAudio(audio, format);
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* Process audio through STT and then LLM
|
|
4060
|
+
*/
|
|
4061
|
+
async processAudio(audio, format) {
|
|
4062
|
+
this.setPhase("transcribing");
|
|
4063
|
+
this.emit({
|
|
4064
|
+
type: "activity_log",
|
|
4065
|
+
payload: {
|
|
4066
|
+
id: uuidv4(),
|
|
4067
|
+
timestamp: new Date(),
|
|
4068
|
+
type: "system",
|
|
4069
|
+
content: "Transcribing audio...",
|
|
4070
|
+
},
|
|
4071
|
+
});
|
|
4072
|
+
try {
|
|
4073
|
+
const requestId = uuidv4();
|
|
4074
|
+
const result = await this.sttManager.transcribe(audio, format, {
|
|
4075
|
+
requestId,
|
|
4076
|
+
label: this.isVoiceMode ? "voice" : "buffered",
|
|
4077
|
+
});
|
|
4078
|
+
const transcriptText = result.text.trim();
|
|
4079
|
+
this.sessionLogger.info({
|
|
4080
|
+
requestId,
|
|
4081
|
+
isVoiceMode: this.isVoiceMode,
|
|
4082
|
+
transcriptLength: transcriptText.length,
|
|
4083
|
+
transcript: transcriptText,
|
|
4084
|
+
}, "Transcription result");
|
|
4085
|
+
await this.handleTranscriptionResultPayload({
|
|
4086
|
+
text: result.text,
|
|
4087
|
+
language: result.language,
|
|
4088
|
+
duration: result.duration,
|
|
4089
|
+
requestId,
|
|
4090
|
+
avgLogprob: result.avgLogprob,
|
|
4091
|
+
isLowConfidence: result.isLowConfidence,
|
|
4092
|
+
byteLength: result.byteLength,
|
|
4093
|
+
format: result.format,
|
|
4094
|
+
debugRecordingPath: result.debugRecordingPath,
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
catch (error) {
|
|
4098
|
+
this.setPhase("idle");
|
|
4099
|
+
this.clearSpeechInProgress("transcription error");
|
|
4100
|
+
this.emit({
|
|
4101
|
+
type: "activity_log",
|
|
4102
|
+
payload: {
|
|
4103
|
+
id: uuidv4(),
|
|
4104
|
+
timestamp: new Date(),
|
|
4105
|
+
type: "error",
|
|
4106
|
+
content: `Transcription error: ${error.message}`,
|
|
4107
|
+
},
|
|
4108
|
+
});
|
|
4109
|
+
throw error;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
async handleTranscriptionResultPayload(result) {
|
|
4113
|
+
const transcriptText = result.text.trim();
|
|
4114
|
+
this.emit({
|
|
4115
|
+
type: "transcription_result",
|
|
4116
|
+
payload: {
|
|
4117
|
+
text: result.text,
|
|
4118
|
+
...(result.language ? { language: result.language } : {}),
|
|
4119
|
+
...(result.duration !== undefined ? { duration: result.duration } : {}),
|
|
4120
|
+
requestId: result.requestId,
|
|
4121
|
+
...(result.avgLogprob !== undefined ? { avgLogprob: result.avgLogprob } : {}),
|
|
4122
|
+
...(result.isLowConfidence !== undefined ? { isLowConfidence: result.isLowConfidence } : {}),
|
|
4123
|
+
...(result.byteLength !== undefined ? { byteLength: result.byteLength } : {}),
|
|
4124
|
+
...(result.format ? { format: result.format } : {}),
|
|
4125
|
+
...(result.debugRecordingPath ? { debugRecordingPath: result.debugRecordingPath } : {}),
|
|
4126
|
+
},
|
|
4127
|
+
});
|
|
4128
|
+
if (!transcriptText) {
|
|
4129
|
+
this.sessionLogger.debug("Empty transcription (false positive), not aborting");
|
|
4130
|
+
this.setPhase("idle");
|
|
4131
|
+
this.clearSpeechInProgress("empty transcription");
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
// Has content - abort any in-progress stream now
|
|
4135
|
+
this.createAbortController();
|
|
4136
|
+
if (result.debugRecordingPath) {
|
|
4137
|
+
this.emit({
|
|
4138
|
+
type: "activity_log",
|
|
4139
|
+
payload: {
|
|
4140
|
+
id: uuidv4(),
|
|
4141
|
+
timestamp: new Date(),
|
|
4142
|
+
type: "system",
|
|
4143
|
+
content: `Saved input audio: ${result.debugRecordingPath}`,
|
|
4144
|
+
metadata: {
|
|
4145
|
+
recordingPath: result.debugRecordingPath,
|
|
4146
|
+
...(result.format ? { format: result.format } : {}),
|
|
4147
|
+
requestId: result.requestId,
|
|
4148
|
+
},
|
|
4149
|
+
},
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
this.emit({
|
|
4153
|
+
type: "activity_log",
|
|
4154
|
+
payload: {
|
|
4155
|
+
id: uuidv4(),
|
|
4156
|
+
timestamp: new Date(),
|
|
4157
|
+
type: "transcript",
|
|
4158
|
+
content: result.text,
|
|
4159
|
+
metadata: {
|
|
4160
|
+
...(result.language ? { language: result.language } : {}),
|
|
4161
|
+
...(result.duration !== undefined ? { duration: result.duration } : {}),
|
|
4162
|
+
},
|
|
4163
|
+
},
|
|
4164
|
+
});
|
|
4165
|
+
this.clearSpeechInProgress("transcription complete");
|
|
4166
|
+
this.setPhase("idle");
|
|
4167
|
+
if (!this.isVoiceMode) {
|
|
4168
|
+
this.sessionLogger.debug({ requestId: result.requestId }, "Skipping voice agent processing because voice mode is disabled");
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
const agentId = this.voiceModeAgentId;
|
|
4172
|
+
if (!agentId) {
|
|
4173
|
+
this.sessionLogger.warn({ requestId: result.requestId }, "Skipping voice agent processing because no agent is currently voice-enabled");
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
// Route voice utterances through the same send path as regular text input:
|
|
4177
|
+
// interrupt-if-running, record message, then start a new stream.
|
|
4178
|
+
await this.handleSendAgentMessage(agentId, result.text);
|
|
4179
|
+
}
|
|
4180
|
+
registerVoiceBridgeForAgent(agentId) {
|
|
4181
|
+
this.registerVoiceSpeakHandler?.(agentId, async ({ text, signal }) => {
|
|
4182
|
+
this.sessionLogger.info({
|
|
4183
|
+
agentId,
|
|
4184
|
+
textLength: text.length,
|
|
4185
|
+
preview: text.slice(0, 160),
|
|
4186
|
+
}, "Voice speak tool call received by session handler");
|
|
4187
|
+
const abortSignal = signal ?? this.abortController.signal;
|
|
4188
|
+
await this.ttsManager.generateAndWaitForPlayback(text, (msg) => this.emit(msg), abortSignal, true);
|
|
4189
|
+
this.sessionLogger.info({ agentId, textLength: text.length }, "Voice speak tool call finished playback");
|
|
4190
|
+
this.emit({
|
|
4191
|
+
type: "activity_log",
|
|
4192
|
+
payload: {
|
|
4193
|
+
id: uuidv4(),
|
|
4194
|
+
timestamp: new Date(),
|
|
4195
|
+
type: "assistant",
|
|
4196
|
+
content: text,
|
|
4197
|
+
},
|
|
4198
|
+
});
|
|
4199
|
+
});
|
|
4200
|
+
this.registerVoiceCallerContext?.(agentId, {
|
|
4201
|
+
childAgentDefaultLabels: { ui: "true" },
|
|
4202
|
+
allowCustomCwd: false,
|
|
4203
|
+
enableVoiceTools: true,
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
4207
|
+
* Handle abort request from client
|
|
4208
|
+
*/
|
|
4209
|
+
async handleAbort() {
|
|
4210
|
+
this.sessionLogger.info({ phase: this.processingPhase }, `Abort request, phase: ${this.processingPhase}`);
|
|
4211
|
+
this.abortController.abort();
|
|
4212
|
+
this.ttsManager.cancelPendingPlaybacks("abort request");
|
|
4213
|
+
// Voice abort should always interrupt active agent output immediately.
|
|
4214
|
+
if (this.isVoiceMode && this.voiceModeAgentId) {
|
|
4215
|
+
try {
|
|
4216
|
+
await this.interruptAgentIfRunning(this.voiceModeAgentId);
|
|
4217
|
+
}
|
|
4218
|
+
catch (error) {
|
|
4219
|
+
this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, "Failed to interrupt active voice-mode agent on abort");
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
if (this.processingPhase === "transcribing") {
|
|
4223
|
+
// Still in STT phase - we'll buffer the next audio
|
|
4224
|
+
this.sessionLogger.debug("Will buffer next audio (currently transcribing)");
|
|
4225
|
+
// Phase stays as 'transcribing', handleAudioChunk will handle buffering
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
// Reset phase to idle and clear pending non-voice buffers.
|
|
4229
|
+
this.setPhase("idle");
|
|
4230
|
+
this.pendingAudioSegments = [];
|
|
4231
|
+
this.clearBufferTimeout();
|
|
4232
|
+
}
|
|
4233
|
+
/**
|
|
4234
|
+
* Handle audio playback confirmation from client
|
|
4235
|
+
*/
|
|
4236
|
+
handleAudioPlayed(id) {
|
|
4237
|
+
this.ttsManager.confirmAudioPlayed(id);
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* Mark speech detection start and abort any active playback/agent run.
|
|
4241
|
+
*/
|
|
4242
|
+
async handleVoiceSpeechStart() {
|
|
4243
|
+
if (this.speechInProgress) {
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
const chunkReceivedAt = Date.now();
|
|
4247
|
+
const phaseBeforeAbort = this.processingPhase;
|
|
4248
|
+
const hadActiveStream = this.hasActiveAgentRun(this.voiceModeAgentId);
|
|
4249
|
+
this.speechInProgress = true;
|
|
4250
|
+
this.sessionLogger.debug("Voice speech detected – aborting playback and active agent run");
|
|
4251
|
+
if (this.pendingAudioSegments.length > 0) {
|
|
4252
|
+
this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, `Dropping ${this.pendingAudioSegments.length} buffered audio segment(s) due to voice speech`);
|
|
4253
|
+
this.pendingAudioSegments = [];
|
|
4254
|
+
}
|
|
4255
|
+
if (this.audioBuffer) {
|
|
4256
|
+
this.sessionLogger.debug({ chunks: this.audioBuffer.chunks.length, pcmBytes: this.audioBuffer.totalPCMBytes }, `Clearing partial audio buffer (${this.audioBuffer.chunks.length} chunk(s)${this.audioBuffer.isPCM
|
|
4257
|
+
? `, ${this.audioBuffer.totalPCMBytes} PCM bytes`
|
|
4258
|
+
: ""})`);
|
|
4259
|
+
this.audioBuffer = null;
|
|
4260
|
+
}
|
|
4261
|
+
this.cancelActiveVoiceDictationStream("new speech turn started");
|
|
4262
|
+
this.clearVoiceModeInactivityTimeout();
|
|
4263
|
+
this.clearBufferTimeout();
|
|
4264
|
+
this.abortController.abort();
|
|
4265
|
+
await this.handleAbort();
|
|
4266
|
+
const latencyMs = Date.now() - chunkReceivedAt;
|
|
4267
|
+
this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, "[Telemetry] barge_in.llm_abort_latency");
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* Clear speech-in-progress flag once the user turn has completed
|
|
4271
|
+
*/
|
|
4272
|
+
clearSpeechInProgress(reason) {
|
|
4273
|
+
if (!this.speechInProgress) {
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
4276
|
+
this.speechInProgress = false;
|
|
4277
|
+
this.sessionLogger.debug({ reason }, `Speech turn complete (${reason}) – resuming TTS`);
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Create new AbortController, aborting the previous one
|
|
4281
|
+
*/
|
|
4282
|
+
createAbortController() {
|
|
4283
|
+
this.abortController.abort();
|
|
4284
|
+
this.abortController = new AbortController();
|
|
4285
|
+
this.ttsDebugStreams.clear();
|
|
4286
|
+
return this.abortController;
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Set the processing phase
|
|
4290
|
+
*/
|
|
4291
|
+
setPhase(phase) {
|
|
4292
|
+
this.processingPhase = phase;
|
|
4293
|
+
this.sessionLogger.debug({ phase }, `Phase: ${phase}`);
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Set timeout to process buffered audio segments
|
|
4297
|
+
*/
|
|
4298
|
+
setBufferTimeout() {
|
|
4299
|
+
this.clearBufferTimeout();
|
|
4300
|
+
this.bufferTimeout = setTimeout(async () => {
|
|
4301
|
+
this.sessionLogger.debug("Buffer timeout reached, processing pending segments");
|
|
4302
|
+
if (this.pendingAudioSegments.length > 0) {
|
|
4303
|
+
const segments = [...this.pendingAudioSegments];
|
|
4304
|
+
this.pendingAudioSegments = [];
|
|
4305
|
+
this.bufferTimeout = null;
|
|
4306
|
+
const combined = Buffer.concat(segments.map((s) => s.audio));
|
|
4307
|
+
await this.processAudio(combined, segments[0].format);
|
|
4308
|
+
}
|
|
4309
|
+
}, 10000); // 10 second timeout
|
|
4310
|
+
}
|
|
4311
|
+
setVoiceModeInactivityTimeout() {
|
|
4312
|
+
if (!this.isVoiceMode) {
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
this.clearVoiceModeInactivityTimeout();
|
|
4316
|
+
this.voiceModeInactivityTimeout = setTimeout(() => {
|
|
4317
|
+
this.voiceModeInactivityTimeout = null;
|
|
4318
|
+
if (!this.isVoiceMode || !this.activeVoiceDictationId) {
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
this.sessionLogger.warn({
|
|
4322
|
+
timeoutMs: VOICE_MODE_INACTIVITY_FLUSH_MS,
|
|
4323
|
+
dictationId: this.activeVoiceDictationId,
|
|
4324
|
+
nextSeq: this.activeVoiceDictationNextSeq,
|
|
4325
|
+
}, "Voice mode inactivity timeout reached without isLast; finalizing active voice dictation stream");
|
|
4326
|
+
void this.finalizeActiveVoiceDictationStream("inactivity timeout").catch((error) => {
|
|
4327
|
+
this.sessionLogger.error({ err: error }, "Failed to finalize voice dictation stream after inactivity timeout");
|
|
4328
|
+
});
|
|
4329
|
+
}, VOICE_MODE_INACTIVITY_FLUSH_MS);
|
|
4330
|
+
}
|
|
4331
|
+
clearVoiceModeInactivityTimeout() {
|
|
4332
|
+
if (this.voiceModeInactivityTimeout) {
|
|
4333
|
+
clearTimeout(this.voiceModeInactivityTimeout);
|
|
4334
|
+
this.voiceModeInactivityTimeout = null;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
/**
|
|
4338
|
+
* Clear buffer timeout
|
|
4339
|
+
*/
|
|
4340
|
+
clearBufferTimeout() {
|
|
4341
|
+
if (this.bufferTimeout) {
|
|
4342
|
+
clearTimeout(this.bufferTimeout);
|
|
4343
|
+
this.bufferTimeout = null;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Emit a message to the client
|
|
4348
|
+
*/
|
|
4349
|
+
emit(msg) {
|
|
4350
|
+
if (msg.type === "audio_output" &&
|
|
4351
|
+
(process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
|
|
4352
|
+
msg.payload.groupId &&
|
|
4353
|
+
typeof msg.payload.audio === "string") {
|
|
4354
|
+
const groupId = msg.payload.groupId;
|
|
4355
|
+
const existing = this.ttsDebugStreams.get(groupId) ??
|
|
4356
|
+
{ format: msg.payload.format, chunks: [] };
|
|
4357
|
+
try {
|
|
4358
|
+
existing.chunks.push(Buffer.from(msg.payload.audio, "base64"));
|
|
4359
|
+
existing.format = msg.payload.format;
|
|
4360
|
+
this.ttsDebugStreams.set(groupId, existing);
|
|
4361
|
+
}
|
|
4362
|
+
catch {
|
|
4363
|
+
// ignore malformed base64
|
|
4364
|
+
}
|
|
4365
|
+
if (msg.payload.isLastChunk) {
|
|
4366
|
+
const final = this.ttsDebugStreams.get(groupId);
|
|
4367
|
+
this.ttsDebugStreams.delete(groupId);
|
|
4368
|
+
if (final && final.chunks.length > 0) {
|
|
4369
|
+
void (async () => {
|
|
4370
|
+
const recordingPath = await maybePersistTtsDebugAudio(Buffer.concat(final.chunks), { sessionId: this.sessionId, groupId, format: final.format }, this.sessionLogger);
|
|
4371
|
+
if (recordingPath) {
|
|
4372
|
+
this.onMessage({
|
|
4373
|
+
type: "activity_log",
|
|
4374
|
+
payload: {
|
|
4375
|
+
id: uuidv4(),
|
|
4376
|
+
timestamp: new Date(),
|
|
4377
|
+
type: "system",
|
|
4378
|
+
content: `Saved TTS audio: ${recordingPath}`,
|
|
4379
|
+
metadata: { recordingPath, format: final.format, groupId },
|
|
4380
|
+
},
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
})();
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
this.onMessage(msg);
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Clean up session resources
|
|
4391
|
+
*/
|
|
4392
|
+
async cleanup() {
|
|
4393
|
+
this.sessionLogger.info("Cleaning up");
|
|
4394
|
+
if (this.unsubscribeAgentEvents) {
|
|
4395
|
+
this.unsubscribeAgentEvents();
|
|
4396
|
+
this.unsubscribeAgentEvents = null;
|
|
4397
|
+
}
|
|
4398
|
+
// Abort any ongoing operations
|
|
4399
|
+
this.abortController.abort();
|
|
4400
|
+
// Clear timeouts
|
|
4401
|
+
this.clearVoiceModeInactivityTimeout();
|
|
4402
|
+
this.clearBufferTimeout();
|
|
4403
|
+
// Clear buffers
|
|
4404
|
+
this.cancelActiveVoiceDictationStream("session cleanup");
|
|
4405
|
+
this.pendingAudioSegments = [];
|
|
4406
|
+
this.audioBuffer = null;
|
|
4407
|
+
// Cleanup managers
|
|
4408
|
+
this.ttsManager.cleanup();
|
|
4409
|
+
this.sttManager.cleanup();
|
|
4410
|
+
this.voiceStreamManager.cleanupAll();
|
|
4411
|
+
this.dictationStreamManager.cleanupAll();
|
|
4412
|
+
// Close MCP clients
|
|
4413
|
+
if (this.agentMcpClient) {
|
|
4414
|
+
try {
|
|
4415
|
+
await this.agentMcpClient.close();
|
|
4416
|
+
this.sessionLogger.debug("Agent MCP client closed");
|
|
4417
|
+
}
|
|
4418
|
+
catch (error) {
|
|
4419
|
+
this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
|
|
4420
|
+
}
|
|
4421
|
+
this.agentMcpClient = null;
|
|
4422
|
+
this.agentTools = null;
|
|
4423
|
+
}
|
|
4424
|
+
await this.disableVoiceModeForActiveAgent(true);
|
|
4425
|
+
this.isVoiceMode = false;
|
|
4426
|
+
// Unsubscribe from all terminals
|
|
4427
|
+
for (const unsubscribe of this.terminalSubscriptions.values()) {
|
|
4428
|
+
unsubscribe();
|
|
4429
|
+
}
|
|
4430
|
+
this.terminalSubscriptions.clear();
|
|
4431
|
+
for (const target of this.checkoutDiffTargets.values()) {
|
|
4432
|
+
this.closeCheckoutDiffWatchTarget(target);
|
|
4433
|
+
}
|
|
4434
|
+
this.checkoutDiffTargets.clear();
|
|
4435
|
+
this.checkoutDiffSubscriptions.clear();
|
|
4436
|
+
}
|
|
4437
|
+
// ============================================================================
|
|
4438
|
+
// Terminal Handlers
|
|
4439
|
+
// ============================================================================
|
|
4440
|
+
async handleListTerminalsRequest(msg) {
|
|
4441
|
+
if (!this.terminalManager) {
|
|
4442
|
+
this.emit({
|
|
4443
|
+
type: "list_terminals_response",
|
|
4444
|
+
payload: {
|
|
4445
|
+
cwd: msg.cwd,
|
|
4446
|
+
terminals: [],
|
|
4447
|
+
requestId: msg.requestId,
|
|
4448
|
+
},
|
|
4449
|
+
});
|
|
4450
|
+
return;
|
|
4451
|
+
}
|
|
4452
|
+
try {
|
|
4453
|
+
const terminals = await this.terminalManager.getTerminals(msg.cwd);
|
|
4454
|
+
this.emit({
|
|
4455
|
+
type: "list_terminals_response",
|
|
4456
|
+
payload: {
|
|
4457
|
+
cwd: msg.cwd,
|
|
4458
|
+
terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
|
|
4459
|
+
requestId: msg.requestId,
|
|
4460
|
+
},
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
catch (error) {
|
|
4464
|
+
this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to list terminals");
|
|
4465
|
+
this.emit({
|
|
4466
|
+
type: "list_terminals_response",
|
|
4467
|
+
payload: {
|
|
4468
|
+
cwd: msg.cwd,
|
|
4469
|
+
terminals: [],
|
|
4470
|
+
requestId: msg.requestId,
|
|
4471
|
+
},
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
async handleCreateTerminalRequest(msg) {
|
|
4476
|
+
if (!this.terminalManager) {
|
|
4477
|
+
this.emit({
|
|
4478
|
+
type: "create_terminal_response",
|
|
4479
|
+
payload: {
|
|
4480
|
+
terminal: null,
|
|
4481
|
+
error: "Terminal manager not available",
|
|
4482
|
+
requestId: msg.requestId,
|
|
4483
|
+
},
|
|
4484
|
+
});
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
try {
|
|
4488
|
+
const session = await this.terminalManager.createTerminal({
|
|
4489
|
+
cwd: msg.cwd,
|
|
4490
|
+
name: msg.name,
|
|
4491
|
+
});
|
|
4492
|
+
this.emit({
|
|
4493
|
+
type: "create_terminal_response",
|
|
4494
|
+
payload: {
|
|
4495
|
+
terminal: { id: session.id, name: session.name, cwd: session.cwd },
|
|
4496
|
+
error: null,
|
|
4497
|
+
requestId: msg.requestId,
|
|
4498
|
+
},
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
catch (error) {
|
|
4502
|
+
this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to create terminal");
|
|
4503
|
+
this.emit({
|
|
4504
|
+
type: "create_terminal_response",
|
|
4505
|
+
payload: {
|
|
4506
|
+
terminal: null,
|
|
4507
|
+
error: error.message,
|
|
4508
|
+
requestId: msg.requestId,
|
|
4509
|
+
},
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
async handleSubscribeTerminalRequest(msg) {
|
|
4514
|
+
if (!this.terminalManager) {
|
|
4515
|
+
this.emit({
|
|
4516
|
+
type: "subscribe_terminal_response",
|
|
4517
|
+
payload: {
|
|
4518
|
+
terminalId: msg.terminalId,
|
|
4519
|
+
state: null,
|
|
4520
|
+
error: "Terminal manager not available",
|
|
4521
|
+
requestId: msg.requestId,
|
|
4522
|
+
},
|
|
4523
|
+
});
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
const session = this.terminalManager.getTerminal(msg.terminalId);
|
|
4527
|
+
if (!session) {
|
|
4528
|
+
this.emit({
|
|
4529
|
+
type: "subscribe_terminal_response",
|
|
4530
|
+
payload: {
|
|
4531
|
+
terminalId: msg.terminalId,
|
|
4532
|
+
state: null,
|
|
4533
|
+
error: "Terminal not found",
|
|
4534
|
+
requestId: msg.requestId,
|
|
4535
|
+
},
|
|
4536
|
+
});
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
// Unsubscribe from previous subscription if any
|
|
4540
|
+
const existing = this.terminalSubscriptions.get(msg.terminalId);
|
|
4541
|
+
if (existing) {
|
|
4542
|
+
existing();
|
|
4543
|
+
}
|
|
4544
|
+
// Subscribe to terminal updates
|
|
4545
|
+
const unsubscribe = session.subscribe((serverMsg) => {
|
|
4546
|
+
if (serverMsg.type === "full") {
|
|
4547
|
+
this.emit({
|
|
4548
|
+
type: "terminal_output",
|
|
4549
|
+
payload: {
|
|
4550
|
+
terminalId: msg.terminalId,
|
|
4551
|
+
state: serverMsg.state,
|
|
4552
|
+
},
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
});
|
|
4556
|
+
this.terminalSubscriptions.set(msg.terminalId, unsubscribe);
|
|
4557
|
+
// Send initial state
|
|
4558
|
+
this.emit({
|
|
4559
|
+
type: "subscribe_terminal_response",
|
|
4560
|
+
payload: {
|
|
4561
|
+
terminalId: msg.terminalId,
|
|
4562
|
+
state: session.getState(),
|
|
4563
|
+
error: null,
|
|
4564
|
+
requestId: msg.requestId,
|
|
4565
|
+
},
|
|
4566
|
+
});
|
|
4567
|
+
}
|
|
4568
|
+
handleUnsubscribeTerminalRequest(msg) {
|
|
4569
|
+
const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
|
|
4570
|
+
if (unsubscribe) {
|
|
4571
|
+
unsubscribe();
|
|
4572
|
+
this.terminalSubscriptions.delete(msg.terminalId);
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
handleTerminalInput(msg) {
|
|
4576
|
+
if (!this.terminalManager) {
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
const session = this.terminalManager.getTerminal(msg.terminalId);
|
|
4580
|
+
if (!session) {
|
|
4581
|
+
this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
|
|
4582
|
+
return;
|
|
4583
|
+
}
|
|
4584
|
+
session.send(msg.message);
|
|
4585
|
+
}
|
|
4586
|
+
async handleKillTerminalRequest(msg) {
|
|
4587
|
+
if (!this.terminalManager) {
|
|
4588
|
+
this.emit({
|
|
4589
|
+
type: "kill_terminal_response",
|
|
4590
|
+
payload: {
|
|
4591
|
+
terminalId: msg.terminalId,
|
|
4592
|
+
success: false,
|
|
4593
|
+
requestId: msg.requestId,
|
|
4594
|
+
},
|
|
4595
|
+
});
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
// Unsubscribe first
|
|
4599
|
+
const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
|
|
4600
|
+
if (unsubscribe) {
|
|
4601
|
+
unsubscribe();
|
|
4602
|
+
this.terminalSubscriptions.delete(msg.terminalId);
|
|
4603
|
+
}
|
|
4604
|
+
this.terminalManager.killTerminal(msg.terminalId);
|
|
4605
|
+
this.emit({
|
|
4606
|
+
type: "kill_terminal_response",
|
|
4607
|
+
payload: {
|
|
4608
|
+
terminalId: msg.terminalId,
|
|
4609
|
+
success: true,
|
|
4610
|
+
requestId: msg.requestId,
|
|
4611
|
+
},
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
//# sourceMappingURL=session.js.map
|