@vellumai/assistant 0.8.1 → 0.8.3
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/ARCHITECTURE.md +13 -19
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +17 -0
- package/docker-init-apt-root.sh +167 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +642 -5
- package/package.json +3 -1
- package/scripts/generate-openapi.ts +83 -10
- package/scripts/sync-llm-catalog.ts +2 -2
- package/scripts/sync-web-search-catalog.ts +47 -25
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +45 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/config-schema.test.ts +21 -0
- package/src/__tests__/config-set-route.test.ts +80 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/context-search-conversations-source.test.ts +117 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +31 -65
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
- package/src/__tests__/conversation-agent-loop.test.ts +59 -1
- package/src/__tests__/conversation-error.test.ts +42 -3
- package/src/__tests__/conversation-fork-crud.test.ts +82 -0
- package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
- package/src/__tests__/conversation-lifecycle.test.ts +173 -0
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +4 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +3 -2
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +151 -55
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +73 -0
- package/src/__tests__/host-app-control-proxy.test.ts +507 -10
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
- package/src/__tests__/inference-profile-reaper.test.ts +4 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
- package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +15 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +58 -13
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +41 -29
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/oauth-commands-routes.test.ts +168 -16
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/openai-provider.test.ts +242 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +10 -36
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +0 -1
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
- package/src/__tests__/plugin-types.test.ts +3 -13
- package/src/__tests__/process-message-background-slack.test.ts +8 -1
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-routes.test.ts +50 -3
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
- package/src/__tests__/skill-load-tool.test.ts +27 -89
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/system-prompt.test.ts +670 -63
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/user-plugin-loader.test.ts +0 -7
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/agent/loop.ts +167 -18
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +122 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +960 -0
- package/src/cli/commands/changelog.ts +106 -42
- package/src/cli/commands/conversations.ts +102 -17
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/notifications.ts +388 -346
- package/src/cli/commands/plugins.ts +252 -0
- package/src/cli/commands/schedules.ts +683 -0
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/install-from-github.ts +303 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +52 -2
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -0
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +41 -9
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +9 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +17 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +23 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1107 -0
- package/src/context/token-estimator.ts +34 -36
- package/src/context/window-manager.ts +197 -1520
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
- package/src/daemon/approval-generators.ts +8 -6
- package/src/daemon/config-watcher.ts +94 -31
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +198 -11
- package/src/daemon/conversation-error.ts +171 -37
- package/src/daemon/conversation-lifecycle.ts +53 -40
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +49 -12
- package/src/daemon/conversation-runtime-assembly.ts +25 -1
- package/src/daemon/conversation-slash.ts +12 -5
- package/src/daemon/conversation-store.ts +11 -4
- package/src/daemon/conversation-tool-setup.ts +39 -7
- package/src/daemon/conversation.ts +33 -8
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +111 -108
- package/src/daemon/history-repair.ts +28 -1
- package/src/daemon/host-app-control-proxy.ts +153 -27
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +89 -91
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +85 -0
- package/src/daemon/message-protocol.ts +1 -0
- package/src/daemon/message-types/conversations.ts +25 -0
- package/src/daemon/message-types/messages.ts +61 -0
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pkb-reminder-builder.test.ts +11 -54
- package/src/daemon/pkb-reminder-builder.ts +5 -20
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +24 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +33 -0
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +73 -189
- package/src/home/__tests__/feed-types.test.ts +80 -0
- package/src/home/feed-types.ts +36 -2
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/ipc/cli-client.ts +147 -45
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
- package/src/memory/__tests__/conversation-queries.test.ts +483 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +42 -10
- package/src/memory/context-search/sources/conversations.ts +62 -2
- package/src/memory/context-search/sources/workspace.ts +4 -0
- package/src/memory/conversation-crud.ts +63 -19
- package/src/memory/conversation-queries.ts +197 -11
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +12 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +4 -4
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
- package/src/memory/graph/conversation-graph-memory.ts +49 -21
- package/src/memory/graph/tools.ts +9 -40
- package/src/memory/indexer.ts +34 -29
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
- package/src/memory/jobs/embed-concept-page.ts +20 -11
- package/src/memory/jobs-worker.ts +6 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
- package/src/memory/message-content.ts +1 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +9 -0
- package/src/memory/migrations/registry.ts +16 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +3 -3
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +288 -11
- package/src/memory/v2/__tests__/migration.test.ts +87 -0
- package/src/memory/v2/__tests__/page-index.test.ts +83 -0
- package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
- package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
- package/src/memory/v2/__tests__/router.test.ts +15 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +81 -26
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +63 -8
- package/src/memory/v2/prompts/router.ts +11 -8
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +135 -7
- package/src/memory/v2/router.ts +9 -8
- package/src/memory/v2/skill-store.ts +120 -35
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +113 -45
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +21 -1
- package/src/notifications/home-feed-side-effect.ts +138 -5
- package/src/notifications/signal.ts +3 -5
- package/src/notifications/types.ts +8 -0
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +19 -6
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +34 -1
- package/src/plugin-api/types.ts +104 -22
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +74 -22
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +187 -42
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +40 -26
- package/src/plugins/user-loader.ts +21 -2
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +37 -5
- package/src/prompts/__tests__/system-prompt.test.ts +10 -43
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -174
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +164 -0
- package/src/providers/__tests__/inference.test.ts +24 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +68 -11
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +32 -6
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +159 -34
- package/src/providers/inference/resolve-auth.ts +14 -4
- package/src/providers/model-catalog.ts +249 -12
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +169 -8
- package/src/providers/openrouter/client.ts +49 -4
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +38 -0
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +52 -15
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +103 -15
- package/src/runtime/auth/route-policy.ts +21 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +19 -47
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +5 -3
- package/src/runtime/routes/auth-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +5 -3
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +126 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-cli-routes.ts +44 -3
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +17 -42
- package/src/runtime/routes/conversation-query-routes.ts +99 -35
- package/src/runtime/routes/conversation-routes.ts +97 -11
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
- package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
- package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +6 -13
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/notification-routes.ts +1 -1
- package/src/runtime/routes/oauth-commands-routes.ts +105 -15
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +4 -7
- package/src/runtime/routes/subagents-routes.ts +98 -18
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +27 -2
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +93 -1
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +9 -1
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +17 -7
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/browser-execution.ts +15 -11
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +107 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/watcher/__tests__/engine.test.ts +22 -0
- package/src/watcher/engine.ts +6 -2
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/migrations/runner.ts +39 -9
- package/src/workspace/migrations/types.ts +4 -0
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-window-manager.test.ts +0 -2481
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
QuestionRequest,
|
|
5
|
+
ServerMessage,
|
|
6
|
+
} from "../daemon/message-protocol.js";
|
|
7
|
+
import type {
|
|
8
|
+
QuestionBatchSubmission,
|
|
9
|
+
QuestionPromptResult,
|
|
10
|
+
} from "./question-prompter.js";
|
|
11
|
+
|
|
12
|
+
// Use a tiny timeout so the setTimeout branch fires quickly in tests
|
|
13
|
+
const mockConfig = {
|
|
14
|
+
timeouts: { permissionTimeoutSec: 0.05 },
|
|
15
|
+
};
|
|
16
|
+
mock.module("../config/loader.js", () => ({
|
|
17
|
+
getConfig: () => mockConfig,
|
|
18
|
+
loadConfig: () => mockConfig,
|
|
19
|
+
invalidateConfigCache: () => {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module("../util/logger.js", () => ({
|
|
23
|
+
getLogger: () => ({
|
|
24
|
+
info: () => {},
|
|
25
|
+
warn: () => {},
|
|
26
|
+
error: () => {},
|
|
27
|
+
debug: () => {},
|
|
28
|
+
trace: () => {},
|
|
29
|
+
fatal: () => {},
|
|
30
|
+
child: () => ({
|
|
31
|
+
info: () => {},
|
|
32
|
+
warn: () => {},
|
|
33
|
+
error: () => {},
|
|
34
|
+
debug: () => {},
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Use a real Map so QuestionPrompter can store and retrieve callbacks.
|
|
40
|
+
interface MockInteraction {
|
|
41
|
+
rpcResolve?: (v: unknown) => void;
|
|
42
|
+
rpcReject?: (e: unknown) => void;
|
|
43
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
44
|
+
metadata?: {
|
|
45
|
+
orderedIds: string[];
|
|
46
|
+
optionsById: Record<string, string[]>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const _piStore = new Map<string, MockInteraction>();
|
|
50
|
+
mock.module("../runtime/pending-interactions.js", () => ({
|
|
51
|
+
register: (id: string, entry: MockInteraction) => _piStore.set(id, entry),
|
|
52
|
+
resolve: (id: string) => {
|
|
53
|
+
const e = _piStore.get(id);
|
|
54
|
+
if (e?.timer != null) clearTimeout(e.timer);
|
|
55
|
+
_piStore.delete(id);
|
|
56
|
+
return e;
|
|
57
|
+
},
|
|
58
|
+
get: (id: string) => _piStore.get(id),
|
|
59
|
+
getAll: () => [..._piStore.values()],
|
|
60
|
+
getByConversation: () => [],
|
|
61
|
+
getByKind: () => [],
|
|
62
|
+
removeByConversation: () => {},
|
|
63
|
+
clear: () => _piStore.clear(),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
QuestionPrompter,
|
|
68
|
+
QuestionBatchValidationError,
|
|
69
|
+
buildBatchEntries,
|
|
70
|
+
} = await import("./question-prompter.js");
|
|
71
|
+
|
|
72
|
+
function makePrompter() {
|
|
73
|
+
const sent: ServerMessage[] = [];
|
|
74
|
+
const prompter = new QuestionPrompter({
|
|
75
|
+
broadcastMessage: (msg) => sent.push(msg),
|
|
76
|
+
});
|
|
77
|
+
return { prompter, sent };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drive a pending question interaction the same way the
|
|
82
|
+
* `/v1/question-response` route does: look up the metadata, run
|
|
83
|
+
* `buildBatchEntries`, deregister, then fire `rpcResolve`. Centralizing the
|
|
84
|
+
* sequence in one helper keeps the tests focused on observable behavior and
|
|
85
|
+
* mirrors the production resolution path.
|
|
86
|
+
*/
|
|
87
|
+
function resolveBatch(
|
|
88
|
+
requestId: string,
|
|
89
|
+
submissions: QuestionBatchSubmission[],
|
|
90
|
+
): QuestionPromptResult {
|
|
91
|
+
const interaction = _piStore.get(requestId);
|
|
92
|
+
if (!interaction?.metadata) {
|
|
93
|
+
throw new Error(`No pending question interaction for ${requestId}`);
|
|
94
|
+
}
|
|
95
|
+
const { orderedIds, optionsById } = interaction.metadata;
|
|
96
|
+
const entries = buildBatchEntries(
|
|
97
|
+
orderedIds,
|
|
98
|
+
(qid, oid) => (optionsById[qid] ?? []).includes(oid),
|
|
99
|
+
new Set(Object.keys(optionsById)),
|
|
100
|
+
submissions,
|
|
101
|
+
);
|
|
102
|
+
const result: QuestionPromptResult = { entries, overall: "completed" };
|
|
103
|
+
if (interaction.timer != null) clearTimeout(interaction.timer);
|
|
104
|
+
_piStore.delete(requestId);
|
|
105
|
+
interaction.rpcResolve?.(result);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Close a pending question card: every entry reported as `skipped`, overall
|
|
111
|
+
* status `closed`. Mirrors the route's `kind: "close"` branch.
|
|
112
|
+
*/
|
|
113
|
+
function closeBatch(requestId: string): QuestionPromptResult {
|
|
114
|
+
const interaction = _piStore.get(requestId);
|
|
115
|
+
if (!interaction?.metadata) {
|
|
116
|
+
throw new Error(`No pending question interaction for ${requestId}`);
|
|
117
|
+
}
|
|
118
|
+
const result: QuestionPromptResult = {
|
|
119
|
+
entries: interaction.metadata.orderedIds.map((id) => ({
|
|
120
|
+
questionId: id,
|
|
121
|
+
decision: "skipped" as const,
|
|
122
|
+
})),
|
|
123
|
+
overall: "closed",
|
|
124
|
+
};
|
|
125
|
+
if (interaction.timer != null) clearTimeout(interaction.timer);
|
|
126
|
+
_piStore.delete(requestId);
|
|
127
|
+
interaction.rpcResolve?.(result);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fruitOptions = [
|
|
132
|
+
{ id: "a", label: "Apple" },
|
|
133
|
+
{ id: "b", label: "Banana" },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const singleQuestionParams = {
|
|
137
|
+
conversationId: "conv-1",
|
|
138
|
+
questions: [
|
|
139
|
+
{
|
|
140
|
+
question: "Pick one",
|
|
141
|
+
options: fruitOptions,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const threeQuestionParams = {
|
|
147
|
+
conversationId: "conv-1",
|
|
148
|
+
questions: [
|
|
149
|
+
{ question: "Q1?", options: fruitOptions },
|
|
150
|
+
{
|
|
151
|
+
question: "Q2?",
|
|
152
|
+
options: [
|
|
153
|
+
{ id: "x", label: "X" },
|
|
154
|
+
{ id: "y", label: "Y" },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
question: "Q3?",
|
|
159
|
+
options: [
|
|
160
|
+
{ id: "p", label: "P" },
|
|
161
|
+
{ id: "q", label: "Q" },
|
|
162
|
+
],
|
|
163
|
+
freeTextPlaceholder: "or type",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
describe("QuestionPrompter", () => {
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
_piStore.clear();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("happy path: option resolution via the shared batch helpers", async () => {
|
|
174
|
+
const { prompter, sent } = makePrompter();
|
|
175
|
+
|
|
176
|
+
const promise = prompter.prompt(singleQuestionParams);
|
|
177
|
+
|
|
178
|
+
expect(sent).toHaveLength(1);
|
|
179
|
+
const req = sent[0] as QuestionRequest;
|
|
180
|
+
expect(req.type).toBe("question_request");
|
|
181
|
+
expect(req.questions).toHaveLength(1);
|
|
182
|
+
expect(req.questions[0]?.id).toBe("q1");
|
|
183
|
+
|
|
184
|
+
resolveBatch(req.requestId, [
|
|
185
|
+
{ questionId: "q1", kind: "option", optionId: "a" },
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const result = await promise;
|
|
189
|
+
expect(result).toEqual({
|
|
190
|
+
entries: [{ questionId: "q1", decision: "option", optionId: "a" }],
|
|
191
|
+
overall: "completed",
|
|
192
|
+
});
|
|
193
|
+
expect(_piStore.has(req.requestId)).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("free-text resolution", async () => {
|
|
197
|
+
const { prompter, sent } = makePrompter();
|
|
198
|
+
|
|
199
|
+
const promise = prompter.prompt({
|
|
200
|
+
conversationId: "conv-1",
|
|
201
|
+
questions: [
|
|
202
|
+
{
|
|
203
|
+
question: "Pick one",
|
|
204
|
+
options: fruitOptions,
|
|
205
|
+
freeTextPlaceholder: "Type a fruit",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const req = sent[0] as QuestionRequest;
|
|
211
|
+
expect(req.freeTextPlaceholder).toBe("Type a fruit");
|
|
212
|
+
expect(req.questions[0]?.freeTextPlaceholder).toBe("Type a fruit");
|
|
213
|
+
|
|
214
|
+
resolveBatch(req.requestId, [
|
|
215
|
+
{ questionId: "q1", kind: "free_text", text: "Cherry" },
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const result = await promise;
|
|
219
|
+
expect(result).toEqual({
|
|
220
|
+
entries: [{ questionId: "q1", decision: "free_text", text: "Cherry" }],
|
|
221
|
+
overall: "completed",
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("batched broadcast: assigns sequential q1..qN ids and mirrors questions[0] in flat fields", async () => {
|
|
226
|
+
const { prompter, sent } = makePrompter();
|
|
227
|
+
|
|
228
|
+
void prompter.prompt(threeQuestionParams);
|
|
229
|
+
|
|
230
|
+
expect(sent).toHaveLength(1);
|
|
231
|
+
const req = sent[0] as QuestionRequest;
|
|
232
|
+
expect(req.questions.map((q) => q.id)).toEqual(["q1", "q2", "q3"]);
|
|
233
|
+
// Flat fields mirror the first entry for backwards compat.
|
|
234
|
+
expect(req.question).toBe("Q1?");
|
|
235
|
+
expect(req.options).toEqual(fruitOptions);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("three-question batch: two options + one free-text → ordered entries", async () => {
|
|
239
|
+
const { prompter, sent } = makePrompter();
|
|
240
|
+
|
|
241
|
+
const promise = prompter.prompt(threeQuestionParams);
|
|
242
|
+
const req = sent[0] as QuestionRequest;
|
|
243
|
+
|
|
244
|
+
resolveBatch(req.requestId, [
|
|
245
|
+
{ questionId: "q2", kind: "option", optionId: "y" },
|
|
246
|
+
{ questionId: "q1", kind: "option", optionId: "a" },
|
|
247
|
+
{ questionId: "q3", kind: "free_text", text: "noon-ish" },
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const result = await promise;
|
|
251
|
+
expect(result.overall).toBe("completed");
|
|
252
|
+
// Result entries are in the original questions[] order, regardless of
|
|
253
|
+
// the order submissions arrive in.
|
|
254
|
+
expect(result.entries).toEqual([
|
|
255
|
+
{ questionId: "q1", decision: "option", optionId: "a" },
|
|
256
|
+
{ questionId: "q2", decision: "option", optionId: "y" },
|
|
257
|
+
{ questionId: "q3", decision: "free_text", text: "noon-ish" },
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("three-question batch: all skipped via submitted entries", async () => {
|
|
262
|
+
const { prompter, sent } = makePrompter();
|
|
263
|
+
|
|
264
|
+
const promise = prompter.prompt(threeQuestionParams);
|
|
265
|
+
const req = sent[0] as QuestionRequest;
|
|
266
|
+
|
|
267
|
+
resolveBatch(req.requestId, [
|
|
268
|
+
{ questionId: "q1", kind: "skip" },
|
|
269
|
+
{ questionId: "q2", kind: "skip" },
|
|
270
|
+
{ questionId: "q3", kind: "skip" },
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
const result = await promise;
|
|
274
|
+
expect(result.overall).toBe("completed");
|
|
275
|
+
expect(result.entries.every((e) => e.decision === "skipped")).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("close path: all entries skipped with overall=closed", async () => {
|
|
279
|
+
const { prompter, sent } = makePrompter();
|
|
280
|
+
|
|
281
|
+
const promise = prompter.prompt(threeQuestionParams);
|
|
282
|
+
const req = sent[0] as QuestionRequest;
|
|
283
|
+
|
|
284
|
+
closeBatch(req.requestId);
|
|
285
|
+
|
|
286
|
+
const result = await promise;
|
|
287
|
+
expect(result.overall).toBe("closed");
|
|
288
|
+
expect(result.entries).toEqual([
|
|
289
|
+
{ questionId: "q1", decision: "skipped" },
|
|
290
|
+
{ questionId: "q2", decision: "skipped" },
|
|
291
|
+
{ questionId: "q3", decision: "skipped" },
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("buildBatchEntries rejects unknown questionId", () => {
|
|
296
|
+
expect(() =>
|
|
297
|
+
buildBatchEntries(
|
|
298
|
+
["q1"],
|
|
299
|
+
() => true,
|
|
300
|
+
new Set(["q1"]),
|
|
301
|
+
[{ questionId: "qX", kind: "option", optionId: "a" }],
|
|
302
|
+
),
|
|
303
|
+
).toThrow(QuestionBatchValidationError);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("buildBatchEntries rejects missing entry", () => {
|
|
307
|
+
expect(() =>
|
|
308
|
+
buildBatchEntries(
|
|
309
|
+
["q1", "q2", "q3"],
|
|
310
|
+
() => true,
|
|
311
|
+
new Set(["q1", "q2", "q3"]),
|
|
312
|
+
[
|
|
313
|
+
{ questionId: "q1", kind: "option", optionId: "a" },
|
|
314
|
+
{ questionId: "q2", kind: "option", optionId: "x" },
|
|
315
|
+
],
|
|
316
|
+
),
|
|
317
|
+
).toThrow(QuestionBatchValidationError);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("buildBatchEntries rejects unknown optionId", () => {
|
|
321
|
+
expect(() =>
|
|
322
|
+
buildBatchEntries(
|
|
323
|
+
["q1"],
|
|
324
|
+
(qid, oid) => qid === "q1" && (oid === "a" || oid === "b"),
|
|
325
|
+
new Set(["q1"]),
|
|
326
|
+
[{ questionId: "q1", kind: "option", optionId: "nope" }],
|
|
327
|
+
),
|
|
328
|
+
).toThrow(QuestionBatchValidationError);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("timeout fires with overall: timed_out and timed_out entries", async () => {
|
|
332
|
+
const { prompter } = makePrompter();
|
|
333
|
+
const result = await prompter.prompt(threeQuestionParams);
|
|
334
|
+
expect(result.overall).toBe("timed_out");
|
|
335
|
+
expect(result.entries).toEqual([
|
|
336
|
+
{ questionId: "q1", decision: "timed_out" },
|
|
337
|
+
{ questionId: "q2", decision: "timed_out" },
|
|
338
|
+
{ questionId: "q3", decision: "timed_out" },
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("abort signal triggers overall: aborted with per-entry aborted decisions", async () => {
|
|
343
|
+
const { prompter, sent } = makePrompter();
|
|
344
|
+
const ac = new AbortController();
|
|
345
|
+
|
|
346
|
+
const promise = prompter.prompt({
|
|
347
|
+
...threeQuestionParams,
|
|
348
|
+
signal: ac.signal,
|
|
349
|
+
});
|
|
350
|
+
const req = sent[0] as QuestionRequest;
|
|
351
|
+
expect(_piStore.has(req.requestId)).toBe(true);
|
|
352
|
+
|
|
353
|
+
ac.abort();
|
|
354
|
+
const result = await promise;
|
|
355
|
+
expect(result.overall).toBe("aborted");
|
|
356
|
+
expect(result.entries).toEqual([
|
|
357
|
+
{ questionId: "q1", decision: "aborted" },
|
|
358
|
+
{ questionId: "q2", decision: "aborted" },
|
|
359
|
+
{ questionId: "q3", decision: "aborted" },
|
|
360
|
+
]);
|
|
361
|
+
expect(_piStore.has(req.requestId)).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("abort after removeByConversation still resolves the Promise (no hang)", async () => {
|
|
365
|
+
// Regression test for the race exposed by post-merge review of #30581:
|
|
366
|
+
// when `removeByConversation()` (auto-deny on enqueue) deregisters the
|
|
367
|
+
// question interaction before the abort signal fires, the abort handler
|
|
368
|
+
// must still resolve the prompt Promise. Previously, the handler used
|
|
369
|
+
// `pendingInteractions.resolve(id) === undefined` as the idempotency
|
|
370
|
+
// guard — which returned `undefined` after the registry was cleared,
|
|
371
|
+
// causing the handler to early-return and the Promise to hang forever.
|
|
372
|
+
// Now an internal `settled` flag guards every resolution path.
|
|
373
|
+
const { prompter, sent } = makePrompter();
|
|
374
|
+
const ac = new AbortController();
|
|
375
|
+
|
|
376
|
+
const promise = prompter.prompt({
|
|
377
|
+
...threeQuestionParams,
|
|
378
|
+
signal: ac.signal,
|
|
379
|
+
});
|
|
380
|
+
const req = sent[0] as QuestionRequest;
|
|
381
|
+
expect(_piStore.has(req.requestId)).toBe(true);
|
|
382
|
+
|
|
383
|
+
// Simulate `removeByConversation` clearing the registry entry before
|
|
384
|
+
// the abort signal fires.
|
|
385
|
+
const interaction = _piStore.get(req.requestId);
|
|
386
|
+
if (interaction?.timer != null) clearTimeout(interaction.timer);
|
|
387
|
+
_piStore.delete(req.requestId);
|
|
388
|
+
|
|
389
|
+
ac.abort();
|
|
390
|
+
const result = await promise;
|
|
391
|
+
expect(result.overall).toBe("aborted");
|
|
392
|
+
expect(result.entries).toEqual([
|
|
393
|
+
{ questionId: "q1", decision: "aborted" },
|
|
394
|
+
{ questionId: "q2", decision: "aborted" },
|
|
395
|
+
{ questionId: "q3", decision: "aborted" },
|
|
396
|
+
]);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("pre-aborted signal short-circuits before broadcasting with aborted entries", async () => {
|
|
400
|
+
const { prompter, sent } = makePrompter();
|
|
401
|
+
const ac = new AbortController();
|
|
402
|
+
ac.abort();
|
|
403
|
+
|
|
404
|
+
const result = await prompter.prompt({
|
|
405
|
+
...threeQuestionParams,
|
|
406
|
+
signal: ac.signal,
|
|
407
|
+
});
|
|
408
|
+
expect(result.overall).toBe("aborted");
|
|
409
|
+
expect(result.entries).toEqual([
|
|
410
|
+
{ questionId: "q1", decision: "aborted" },
|
|
411
|
+
{ questionId: "q2", decision: "aborted" },
|
|
412
|
+
{ questionId: "q3", decision: "aborted" },
|
|
413
|
+
]);
|
|
414
|
+
expect(sent).toHaveLength(0);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
|
|
3
|
+
import { getConfig } from "../config/loader.js";
|
|
4
|
+
import type {
|
|
5
|
+
QuestionOption,
|
|
6
|
+
QuestionRequest,
|
|
7
|
+
ServerMessage,
|
|
8
|
+
} from "../daemon/message-protocol.js";
|
|
9
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
10
|
+
import { AssistantError, ErrorCode } from "../util/errors.js";
|
|
11
|
+
import { getLogger } from "../util/logger.js";
|
|
12
|
+
|
|
13
|
+
const log = getLogger("question-prompter");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a batched submission fails validation (unknown questionId,
|
|
17
|
+
* missing entries, unknown optionId, duplicate questionId). The route layer
|
|
18
|
+
* maps this to a 400.
|
|
19
|
+
*/
|
|
20
|
+
export class QuestionBatchValidationError extends Error {
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "QuestionBatchValidationError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One per-question entry in a batched question result.
|
|
29
|
+
*
|
|
30
|
+
* `decision` records how the user responded to that specific question:
|
|
31
|
+
* - `"option"` / `"free_text"` — direct answer.
|
|
32
|
+
* - `"skipped"` — the user explicitly skipped this question, or the card
|
|
33
|
+
* was closed before any answer was submitted.
|
|
34
|
+
* - `"timed_out"` — the prompt timer fired before the client submitted.
|
|
35
|
+
* - `"aborted"` — the prompter's abort signal fired before any answer
|
|
36
|
+
* was submitted.
|
|
37
|
+
*
|
|
38
|
+
* `questionId` matches the daemon-assigned id (`q1`, `q2`...) that the
|
|
39
|
+
* prompter attached to the broadcast.
|
|
40
|
+
*/
|
|
41
|
+
export interface QuestionPromptEntryResult {
|
|
42
|
+
questionId: string;
|
|
43
|
+
decision: "option" | "free_text" | "skipped" | "timed_out" | "aborted";
|
|
44
|
+
optionId?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Aggregate result for a single `prompt()` call. `entries` is ordered to
|
|
50
|
+
* match the original `questions` array; `overall` summarizes how the
|
|
51
|
+
* card lifecycle ended.
|
|
52
|
+
*/
|
|
53
|
+
export interface QuestionPromptResult {
|
|
54
|
+
entries: QuestionPromptEntryResult[];
|
|
55
|
+
overall: "completed" | "closed" | "timed_out" | "aborted";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface QuestionPromptParamsEntry {
|
|
59
|
+
question: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
options: QuestionOption[];
|
|
62
|
+
freeTextPlaceholder?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface QuestionPromptParams {
|
|
66
|
+
conversationId: string;
|
|
67
|
+
/** One or more clarifying questions to broadcast as a single card. */
|
|
68
|
+
questions: QuestionPromptParamsEntry[];
|
|
69
|
+
toolUseId?: string;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One per-question submission inside a batch from the client. */
|
|
74
|
+
export type QuestionBatchSubmission =
|
|
75
|
+
| { questionId: string; kind: "option"; optionId: string }
|
|
76
|
+
| { questionId: string; kind: "free_text"; text: string }
|
|
77
|
+
| { questionId: string; kind: "skip" };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate a batched submission against the original ordered ids and per-id
|
|
81
|
+
* option-id sets, and return the ordered per-entry result. The lookup helpers
|
|
82
|
+
* are passed in so callers can back the metadata with whatever container
|
|
83
|
+
* they prefer (Set/Map, plain Record, etc.).
|
|
84
|
+
*
|
|
85
|
+
* Throws {@link QuestionBatchValidationError} if validation fails.
|
|
86
|
+
*/
|
|
87
|
+
export function buildBatchEntries(
|
|
88
|
+
orderedIds: readonly string[],
|
|
89
|
+
isKnownOption: (questionId: string, optionId: string) => boolean,
|
|
90
|
+
knownQuestionIds: ReadonlySet<string>,
|
|
91
|
+
submissions: readonly QuestionBatchSubmission[],
|
|
92
|
+
): QuestionPromptEntryResult[] {
|
|
93
|
+
const submittedIds = new Set<string>();
|
|
94
|
+
for (const s of submissions) {
|
|
95
|
+
if (!knownQuestionIds.has(s.questionId)) {
|
|
96
|
+
throw new QuestionBatchValidationError(
|
|
97
|
+
`Unknown questionId in batch: ${s.questionId}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (submittedIds.has(s.questionId)) {
|
|
101
|
+
throw new QuestionBatchValidationError(
|
|
102
|
+
`Duplicate questionId in batch: ${s.questionId}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
submittedIds.add(s.questionId);
|
|
106
|
+
if (s.kind === "option" && !isKnownOption(s.questionId, s.optionId)) {
|
|
107
|
+
throw new QuestionBatchValidationError(
|
|
108
|
+
`Unknown optionId "${s.optionId}" for question ${s.questionId}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const id of orderedIds) {
|
|
113
|
+
if (!submittedIds.has(id)) {
|
|
114
|
+
throw new QuestionBatchValidationError(
|
|
115
|
+
`Missing response for questionId ${id}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const byId = new Map<string, QuestionBatchSubmission>();
|
|
121
|
+
for (const s of submissions) byId.set(s.questionId, s);
|
|
122
|
+
|
|
123
|
+
return orderedIds.map((id) => {
|
|
124
|
+
const s = byId.get(id)!;
|
|
125
|
+
if (s.kind === "option") {
|
|
126
|
+
return { questionId: id, decision: "option", optionId: s.optionId };
|
|
127
|
+
}
|
|
128
|
+
if (s.kind === "free_text") {
|
|
129
|
+
return { questionId: id, decision: "free_text", text: s.text };
|
|
130
|
+
}
|
|
131
|
+
return { questionId: id, decision: "skipped" };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Shape of the per-batch bookkeeping stashed on `PendingInteraction.metadata`.
|
|
137
|
+
* The route reads this to validate batched submissions without needing a
|
|
138
|
+
* reference to the prompter that registered them.
|
|
139
|
+
*/
|
|
140
|
+
export interface QuestionBatchMetadata {
|
|
141
|
+
orderedIds: string[];
|
|
142
|
+
optionsById: Record<string, string[]>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Broadcast an ask-question request to all connected clients and wait for the
|
|
147
|
+
* user's reply. All lifecycle state (rpcResolve, rpcReject, timer, batch
|
|
148
|
+
* metadata) lives on the `pendingInteractions` entry — `/v1/question-response`
|
|
149
|
+
* resolves the entry directly without holding a reference back to the prompter
|
|
150
|
+
* that registered it.
|
|
151
|
+
*
|
|
152
|
+
* Batching: a single `prompt()` call broadcasts one or more questions, and
|
|
153
|
+
* the prompter waits for exactly one resolution call carrying the full
|
|
154
|
+
* ordered response array. The web UI collects per-question answers
|
|
155
|
+
* locally, lets the user revise freely while the card is open, and POSTs
|
|
156
|
+
* the whole batch to `/v1/question-response` when the user is done — no
|
|
157
|
+
* per-question accumulator, no partial state machine.
|
|
158
|
+
*
|
|
159
|
+
* Timeout reuses `getConfig().timeouts.permissionTimeoutSec` (default 5 min) —
|
|
160
|
+
* questions are user-prompts in the same UX family as permission prompts and
|
|
161
|
+
* secret prompts, so they share the same idle-timeout knob.
|
|
162
|
+
*/
|
|
163
|
+
export class QuestionPrompter {
|
|
164
|
+
constructor(
|
|
165
|
+
private deps: { broadcastMessage(msg: ServerMessage): void },
|
|
166
|
+
) {}
|
|
167
|
+
|
|
168
|
+
async prompt(params: QuestionPromptParams): Promise<QuestionPromptResult> {
|
|
169
|
+
const { conversationId, questions, toolUseId, signal } = params;
|
|
170
|
+
|
|
171
|
+
if (questions.length === 0) {
|
|
172
|
+
throw new AssistantError(
|
|
173
|
+
"QuestionPrompter.prompt requires at least one question",
|
|
174
|
+
ErrorCode.INTERNAL_ERROR,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Assign per-question ids (`q1`, `q2`, ...) — daemon-side only; the LLM
|
|
179
|
+
// never sees these. Build the on-wire entries in the same pass.
|
|
180
|
+
const entries = questions.map((q, i) => ({
|
|
181
|
+
id: `q${i + 1}`,
|
|
182
|
+
question: q.question,
|
|
183
|
+
description: q.description,
|
|
184
|
+
options: q.options,
|
|
185
|
+
freeTextPlaceholder: q.freeTextPlaceholder,
|
|
186
|
+
}));
|
|
187
|
+
const orderedIds = entries.map((e) => e.id);
|
|
188
|
+
const optionsById: Record<string, string[]> = {};
|
|
189
|
+
for (const e of entries) {
|
|
190
|
+
optionsById[e.id] = e.options.map((o) => o.id);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (signal?.aborted) {
|
|
194
|
+
return {
|
|
195
|
+
entries: orderedIds.map((id) => ({
|
|
196
|
+
questionId: id,
|
|
197
|
+
decision: "aborted",
|
|
198
|
+
})),
|
|
199
|
+
overall: "aborted",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const requestId = uuid();
|
|
204
|
+
|
|
205
|
+
return new Promise<QuestionPromptResult>((resolve, reject) => {
|
|
206
|
+
const timeoutMs = getConfig().timeouts.permissionTimeoutSec * 1000;
|
|
207
|
+
|
|
208
|
+
// Closure-scoped idempotency guard. Every resolution path (timeout,
|
|
209
|
+
// abort, route resolution via `rpcResolve`/`rpcReject`) routes through
|
|
210
|
+
// `finish()`, which tears down the timer + abort listener exactly
|
|
211
|
+
// once. We cannot use `pendingInteractions.resolve(requestId) ===
|
|
212
|
+
// undefined` as the guard because `removeByConversation()` (called
|
|
213
|
+
// during auto-deny on enqueue) can deregister the entry before any of
|
|
214
|
+
// our local handlers fire — using the registry as the guard in that
|
|
215
|
+
// case would leave the Promise unresolved and the tool hung.
|
|
216
|
+
let settled = false;
|
|
217
|
+
let onAbort: (() => void) | undefined;
|
|
218
|
+
const finish = (fn: () => void): void => {
|
|
219
|
+
if (settled) return;
|
|
220
|
+
settled = true;
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
if (signal && onAbort) {
|
|
223
|
+
signal.removeEventListener("abort", onAbort);
|
|
224
|
+
}
|
|
225
|
+
// Idempotent: a no-op if the entry was already removed (e.g. by
|
|
226
|
+
// `removeByConversation`) or by an earlier path.
|
|
227
|
+
pendingInteractions.resolve(requestId);
|
|
228
|
+
fn();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const timer = setTimeout(() => {
|
|
232
|
+
log.warn({ requestId, conversationId }, "Question prompt timed out");
|
|
233
|
+
finish(() =>
|
|
234
|
+
resolve({
|
|
235
|
+
entries: orderedIds.map((id) => ({
|
|
236
|
+
questionId: id,
|
|
237
|
+
decision: "timed_out",
|
|
238
|
+
})),
|
|
239
|
+
overall: "timed_out",
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
}, timeoutMs);
|
|
243
|
+
|
|
244
|
+
if (signal) {
|
|
245
|
+
onAbort = () => {
|
|
246
|
+
finish(() =>
|
|
247
|
+
resolve({
|
|
248
|
+
entries: orderedIds.map((id) => ({
|
|
249
|
+
questionId: id,
|
|
250
|
+
decision: "aborted",
|
|
251
|
+
})),
|
|
252
|
+
overall: "aborted",
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Stash the per-question metadata on the interaction so the route can
|
|
260
|
+
// validate batched submissions without holding a prompter reference.
|
|
261
|
+
// Route resolution funnels through `finish()` so the same teardown +
|
|
262
|
+
// idempotency guard applies whether the response comes from the route,
|
|
263
|
+
// a timeout, or an abort.
|
|
264
|
+
pendingInteractions.register(requestId, {
|
|
265
|
+
conversationId,
|
|
266
|
+
kind: "question",
|
|
267
|
+
rpcResolve: (value: unknown) =>
|
|
268
|
+
finish(() => resolve(value as QuestionPromptResult)),
|
|
269
|
+
rpcReject: (err: unknown) => finish(() => reject(err)),
|
|
270
|
+
timer,
|
|
271
|
+
toolUseId,
|
|
272
|
+
metadata: { orderedIds, optionsById } satisfies QuestionBatchMetadata,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Populate both shapes on the wire: `questions[]` is the canonical
|
|
276
|
+
// batched payload, and the flat fields mirror `questions[0]` for
|
|
277
|
+
// backwards compat with clients that haven't adopted `questions[]`.
|
|
278
|
+
const head = entries[0]!;
|
|
279
|
+
const msg: QuestionRequest = {
|
|
280
|
+
type: "question_request",
|
|
281
|
+
requestId,
|
|
282
|
+
questions: entries,
|
|
283
|
+
question: head.question,
|
|
284
|
+
description: head.description,
|
|
285
|
+
options: head.options,
|
|
286
|
+
freeTextPlaceholder: head.freeTextPlaceholder,
|
|
287
|
+
conversationId,
|
|
288
|
+
toolUseId,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
this.deps.broadcastMessage(msg);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|