@vellumai/assistant 0.8.4 → 0.8.5
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 +2 -2
- package/docs/browser-use-architecture-phase2.md +1 -1
- package/knip.json +2 -1
- package/openapi.yaml +809 -11
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +34 -37
- package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
- package/src/__tests__/audit-log-rotation.test.ts +70 -16
- package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
- package/src/__tests__/btw-routes.test.ts +2 -3
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +3 -3
- package/src/__tests__/checker.test.ts +6 -15
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
- package/src/__tests__/computer-use-tools.test.ts +2 -4
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
- package/src/__tests__/conversation-agent-loop.test.ts +163 -122
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
- package/src/__tests__/conversation-clear-safety.test.ts +25 -25
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
- package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
- package/src/__tests__/conversation-error.test.ts +31 -0
- package/src/__tests__/conversation-fork-crud.test.ts +178 -15
- package/src/__tests__/conversation-lifecycle.test.ts +52 -11
- package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
- package/src/__tests__/conversation-skill-tools.test.ts +2 -5
- package/src/__tests__/conversation-store.test.ts +1 -1
- package/src/__tests__/conversation-sync-tags.test.ts +99 -32
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
- package/src/__tests__/credential-execution-tools.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/dynamic-page-surface.test.ts +2 -2
- package/src/__tests__/email-html-renderer.test.ts +12 -0
- package/src/__tests__/gateway-flag-listener.test.ts +237 -0
- package/src/__tests__/gemini-provider.test.ts +78 -0
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +7 -5
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
- package/src/__tests__/heartbeat-service.test.ts +4 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
- package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
- package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
- package/src/__tests__/llm-resolver.test.ts +77 -9
- package/src/__tests__/llm-usage-store.test.ts +66 -0
- package/src/__tests__/logger.test.ts +89 -0
- package/src/__tests__/mcp-abort-signal.test.ts +2 -2
- package/src/__tests__/media-generate-image.test.ts +31 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
- package/src/__tests__/model-intents.test.ts +2 -4
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/onboarding-template-contract.test.ts +1 -1
- package/src/__tests__/openai-provider.test.ts +46 -0
- package/src/__tests__/openai-responses-provider.test.ts +114 -12
- package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
- package/src/__tests__/platform.test.ts +2 -2
- package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
- package/src/__tests__/plugin-bootstrap.test.ts +2 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
- package/src/__tests__/plugin-types.test.ts +3 -2
- package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
- package/src/__tests__/pricing.test.ts +12 -0
- package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
- package/src/__tests__/registry.test.ts +2 -8
- package/src/__tests__/require-fresh-approval.test.ts +2 -2
- package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
- package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
- package/src/__tests__/skill-tool-factory.test.ts +1 -1
- package/src/__tests__/subagent-notify-parent.test.ts +1 -1
- package/src/__tests__/suggestion-routes.test.ts +1 -0
- package/src/__tests__/sync-message-contract.test.ts +59 -0
- package/src/__tests__/system-prompt.test.ts +145 -131
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/tool-approval-handler.test.ts +1 -5
- package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
- package/src/__tests__/tool-executor.test.ts +9 -62
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
- package/src/__tests__/usage-routes.test.ts +3 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +6 -5
- package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
- package/src/acp/prepare-agent-env.ts +78 -0
- package/src/acp/session-manager.ts +1 -1
- package/src/agent/loop.ts +8 -0
- package/src/api/README.md +5 -0
- package/src/api/index.ts +4 -0
- package/src/api/package.json +10 -0
- package/src/background-wake/background-wake-routes.test.ts +233 -0
- package/src/background-wake/runtime-registry.ts +24 -0
- package/src/cli/commands/__tests__/browser.test.ts +23 -5
- package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
- package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
- package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
- package/src/cli/commands/browser.ts +247 -0
- package/src/cli/commands/domain.ts +91 -41
- package/src/cli/commands/inference.ts +93 -40
- package/src/cli/commands/memory-v2-compare-render.ts +115 -0
- package/src/cli/commands/memory-v2.ts +176 -1
- package/src/cli/commands/memory-v3-render.ts +344 -0
- package/src/cli/commands/memory-v3.ts +316 -0
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +21 -9
- package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
- package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +7 -6
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
- package/src/config/schemas/call-site-catalog.ts +21 -7
- package/src/config/schemas/llm.ts +12 -1
- package/src/config/schemas/memory-v2.ts +246 -0
- package/src/config/schemas/memory.ts +2 -1
- package/src/context/compactor.ts +52 -0
- package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
- package/src/conversations/message-consolidation.ts +404 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
- package/src/daemon/conversation-agent-loop.ts +126 -76
- package/src/daemon/conversation-error.ts +31 -1
- package/src/daemon/conversation-lifecycle.ts +27 -22
- package/src/daemon/conversation-runtime-assembly.ts +10 -9
- package/src/daemon/conversation-tool-setup.ts +63 -3
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +14 -29
- package/src/daemon/disk-pressure-guard.ts +14 -2
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +11 -3
- package/src/daemon/host-browser-proxy.ts +5 -5
- package/src/daemon/host-cu-proxy.ts +4 -4
- package/src/daemon/host-file-proxy.ts +4 -4
- package/src/daemon/host-proxy-base.ts +4 -4
- package/src/daemon/host-transfer-proxy.ts +10 -10
- package/src/daemon/lifecycle.ts +23 -20
- package/src/daemon/meet-manifest-loader.ts +1 -7
- package/src/daemon/message-types/conversations.ts +6 -9
- package/src/daemon/message-types/home.ts +1 -13
- package/src/daemon/message-types/messages.ts +6 -14
- package/src/daemon/message-types/sync.ts +14 -0
- package/src/daemon/shutdown-handlers.ts +24 -5
- package/src/daemon/switch-inference-profile-tool.ts +52 -0
- package/src/daemon/tool-setup-types.ts +13 -0
- package/src/events/relationship-state-updated.ts +25 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/home-greeting.ts +0 -9
- package/src/home/suggested-prompts.ts +0 -9
- package/src/ipc/gateway-flag-listener.ts +123 -0
- package/src/ipc/skill-routes/registries.ts +8 -12
- package/src/memory/__tests__/db-async-query.test.ts +165 -0
- package/src/memory/__tests__/db-maintenance.test.ts +115 -0
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
- package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
- package/src/memory/auto-analysis-enqueue.ts +5 -1
- package/src/memory/conversation-crud.ts +71 -70
- package/src/memory/conversation-starters-cadence.ts +3 -1
- package/src/memory/conversation-title-service.ts +19 -3
- package/src/memory/db-async-query.ts +214 -0
- package/src/memory/db-init.ts +10 -0
- package/src/memory/db-maintenance.ts +30 -21
- package/src/memory/graph/bootstrap.ts +8 -1
- package/src/memory/graph/capability-seed.ts +7 -3
- package/src/memory/graph/conversation-graph-memory.ts +100 -17
- package/src/memory/graph/extraction.ts +1 -5
- package/src/memory/graph/graph-search.ts +7 -1
- package/src/memory/indexer.ts +28 -18
- package/src/memory/job-handlers/cleanup.ts +76 -18
- package/src/memory/job-handlers/conversation-starters.ts +1 -4
- package/src/memory/jobs/embed-pkb-file.ts +6 -1
- package/src/memory/jobs-store.ts +14 -0
- package/src/memory/jobs-worker.ts +55 -22
- package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
- package/src/memory/llm-request-log-source-local.ts +7 -0
- package/src/memory/llm-request-log-source.ts +9 -2
- package/src/memory/llm-request-log-store.ts +43 -1
- package/src/memory/llm-usage-store.ts +24 -0
- package/src/memory/memory-retrospective-enqueue.ts +8 -1
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/memory-v2-activation-log-store.ts +15 -6
- package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
- package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
- package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
- package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
- package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +33 -0
- package/src/memory/schema/conversations.ts +1 -1
- package/src/memory/schema/infrastructure.ts +21 -0
- package/src/memory/tool-usage-store.ts +36 -8
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
- package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
- package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
- package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
- package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
- package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
- package/src/memory/v2/__tests__/injection.test.ts +127 -98
- package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
- package/src/memory/v2/__tests__/router.test.ts +171 -3
- package/src/memory/v2/harness/compare.ts +57 -0
- package/src/memory/v2/harness/metrics.ts +124 -0
- package/src/memory/v2/harness/oracle.ts +145 -0
- package/src/memory/v2/harness/replay-input.ts +224 -0
- package/src/memory/v2/harness/retriever.ts +74 -0
- package/src/memory/v2/harness/router-retriever.ts +43 -0
- package/src/memory/v2/harness/runner.ts +106 -0
- package/src/memory/v2/harness/trace.ts +58 -0
- package/src/memory/v2/injection.ts +21 -15
- package/src/memory/v2/prompts/router.ts +26 -1
- package/src/memory/v2/qdrant.ts +14 -2
- package/src/memory/v2/router.ts +171 -18
- package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
- package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
- package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
- package/src/memory/v3/__tests__/edges.test.ts +563 -0
- package/src/memory/v3/__tests__/filter.test.ts +512 -0
- package/src/memory/v3/__tests__/gate.test.ts +574 -0
- package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
- package/src/memory/v3/__tests__/loop.test.ts +530 -0
- package/src/memory/v3/__tests__/retriever.test.ts +226 -0
- package/src/memory/v3/__tests__/scouts.test.ts +440 -0
- package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
- package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
- package/src/memory/v3/__tests__/traversal.test.ts +469 -0
- package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
- package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
- package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
- package/src/memory/v3/__tests__/validate.test.ts +245 -0
- package/src/memory/v3/auto-edges.ts +223 -0
- package/src/memory/v3/coactivation-store.ts +124 -0
- package/src/memory/v3/consolidation-job.ts +323 -0
- package/src/memory/v3/edge-learning-job.ts +160 -0
- package/src/memory/v3/edges.ts +249 -0
- package/src/memory/v3/filter.ts +281 -0
- package/src/memory/v3/gate.ts +334 -0
- package/src/memory/v3/index-composition.ts +113 -0
- package/src/memory/v3/llm-capture.ts +46 -0
- package/src/memory/v3/loop.ts +382 -0
- package/src/memory/v3/maintenance.ts +144 -0
- package/src/memory/v3/prompt-context.ts +33 -0
- package/src/memory/v3/prompts/consolidation.ts +458 -0
- package/src/memory/v3/prompts/system-prompts.ts +196 -0
- package/src/memory/v3/retriever.ts +33 -0
- package/src/memory/v3/scouts.ts +420 -0
- package/src/memory/v3/shadow-middleware.ts +305 -0
- package/src/memory/v3/traversal.ts +206 -0
- package/src/memory/v3/tree-index.ts +237 -0
- package/src/memory/v3/tree-store.ts +394 -0
- package/src/memory/v3/tree-walk.ts +351 -0
- package/src/memory/v3/types.ts +65 -0
- package/src/memory/v3/validate.ts +300 -0
- package/src/notifications/adapters/macos.ts +18 -1
- package/src/notifications/adapters/platform.ts +1 -1
- package/src/notifications/decision-engine.ts +1 -4
- package/src/notifications/emit-signal.ts +29 -49
- package/src/permissions/prompter.ts +3 -3
- package/src/permissions/question-prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +2 -2
- package/src/plugin-api/index.ts +4 -0
- package/src/plugin-api/types.ts +7 -33
- package/src/plugins/defaults/index.ts +6 -0
- package/src/plugins/defaults/injectors.ts +18 -11
- package/src/plugins/external-plugin-loader.ts +5 -68
- package/src/plugins/types.ts +11 -16
- package/src/proactive-artifact/aux-message-injector.ts +17 -4
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
- package/src/prompts/persona-resolver.ts +36 -21
- package/src/prompts/sections.ts +39 -7
- package/src/prompts/system-prompt.ts +50 -185
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +230 -8
- package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
- package/src/providers/__tests__/retry-callsite.test.ts +85 -5
- package/src/providers/anthropic/client.ts +32 -66
- package/src/providers/call-site-routing.ts +14 -2
- package/src/providers/connection-model-compat.ts +38 -0
- package/src/providers/connection-resolution.ts +16 -2
- package/src/providers/gemini/client.ts +49 -6
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/minimax/client.ts +106 -0
- package/src/providers/model-catalog.ts +43 -0
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openai/chat-completions-provider.ts +6 -3
- package/src/providers/openai/codex-models.ts +18 -0
- package/src/providers/openai/responses-provider.ts +78 -21
- package/src/providers/provider-send-message.ts +7 -1
- package/src/providers/retry.ts +34 -3
- package/src/providers/thinking-config.ts +26 -1
- package/src/providers/usage-tracking.ts +2 -0
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/agent-wake.ts +1 -0
- package/src/runtime/assistant-event-hub.ts +76 -6
- package/src/runtime/auth/route-policy.ts +36 -0
- package/src/runtime/btw-sidechain.ts +0 -6
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +10 -3
- package/src/runtime/pending-interactions.ts +0 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
- package/src/runtime/routes/acp-routes.test.ts +255 -6
- package/src/runtime/routes/acp-routes.ts +8 -1
- package/src/runtime/routes/avatar-routes.ts +10 -10
- package/src/runtime/routes/background-wake-routes.ts +188 -0
- package/src/runtime/routes/browser-tabs-routes.ts +200 -0
- package/src/runtime/routes/btw-routes.ts +0 -6
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +12 -4
- package/src/runtime/routes/conversation-management-routes.ts +77 -20
- package/src/runtime/routes/conversation-query-routes.ts +142 -36
- package/src/runtime/routes/conversation-routes.ts +252 -410
- package/src/runtime/routes/conversation-starter-routes.ts +6 -3
- package/src/runtime/routes/disk-pressure-routes.ts +1 -1
- package/src/runtime/routes/domain-routes.ts +60 -10
- package/src/runtime/routes/email-routes.ts +5 -2
- package/src/runtime/routes/events-routes.ts +54 -10
- package/src/runtime/routes/group-routes.ts +24 -8
- package/src/runtime/routes/host-browser-routes.ts +10 -2
- package/src/runtime/routes/host-cu-routes.ts +2 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
- package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
- package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
- package/src/runtime/routes/memory-item-routes.ts +8 -3
- package/src/runtime/routes/memory-v2-routes.ts +215 -5
- package/src/runtime/routes/memory-v3-routes.ts +316 -0
- package/src/runtime/routes/migration-routes.ts +21 -24
- package/src/runtime/routes/plugins-routes.ts +337 -0
- package/src/runtime/routes/rename-conversation-routes.ts +6 -2
- package/src/runtime/routes/secret-routes.ts +25 -5
- package/src/runtime/routes/settings-routes.ts +12 -11
- package/src/runtime/routes/slack-channel-routes.ts +5 -4
- package/src/runtime/routes/workspace-routes.ts +25 -10
- package/src/runtime/sync/resource-sync-events.ts +106 -38
- package/src/runtime/sync/sync-publisher.test.ts +49 -0
- package/src/runtime/sync/sync-publisher.ts +2 -1
- package/src/runtime/verification-outbound-actions.ts +73 -1
- package/src/telemetry/types.ts +12 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/acp/spawn.test.ts +119 -0
- package/src/tools/acp/spawn.ts +15 -2
- package/src/tools/apps/definitions.ts +2 -8
- package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
- package/src/tools/ask-question/ask-question-tool.ts +38 -45
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
- package/src/tools/browser/browser-execution.ts +16 -3
- package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
- package/src/tools/browser/cdp-client/factory.ts +100 -17
- package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
- package/src/tools/browser/cdp-client/types.ts +65 -0
- package/src/tools/browser/pinned-tabs.ts +96 -40
- package/src/tools/computer-use/definitions.ts +22 -78
- package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
- package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
- package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
- package/src/tools/credentials/vault.ts +3 -9
- package/src/tools/document/document-tool.ts +59 -0
- package/src/tools/execution-target.ts +21 -23
- package/src/tools/executor.ts +6 -1
- package/src/tools/filesystem/edit.ts +3 -9
- package/src/tools/filesystem/list.ts +3 -9
- package/src/tools/filesystem/read.ts +3 -9
- package/src/tools/filesystem/write.ts +3 -9
- package/src/tools/host-filesystem/edit.ts +3 -9
- package/src/tools/host-filesystem/read.ts +3 -9
- package/src/tools/host-filesystem/transfer.ts +3 -9
- package/src/tools/host-filesystem/write.ts +3 -9
- package/src/tools/host-terminal/host-shell.ts +3 -9
- package/src/tools/mcp/mcp-tool-factory.ts +1 -8
- package/src/tools/memory/register.test.ts +1 -1
- package/src/tools/memory/register.ts +4 -9
- package/src/tools/network/web-fetch.ts +3 -9
- package/src/tools/network/web-search.ts +25 -32
- package/src/tools/registry.ts +7 -23
- package/src/tools/schema-transforms.ts +1 -1
- package/src/tools/skills/execute.ts +3 -9
- package/src/tools/skills/load.ts +3 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -8
- package/src/tools/subagent/notify-parent.ts +3 -9
- package/src/tools/system/request-permission.ts +3 -9
- package/src/tools/terminal/shell.ts +3 -9
- package/src/tools/tool-defaults.ts +94 -0
- package/src/tools/types.ts +27 -98
- package/src/tools/ui-surface/definitions.ts +6 -22
- package/src/usage/pricing.ts +23 -0
- package/src/usage/types.ts +12 -0
- package/src/util/logger.ts +16 -7
- package/src/util/platform.ts +7 -2
- package/src/util/sqlite3-runtime.ts +65 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
- package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
- package/src/__tests__/message-complete-display-id.test.ts +0 -175
- package/src/daemon/query-complexity-router.ts +0 -75
- package/src/prompts/cache-boundary.ts +0 -8
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
} from "../../channels/types.js";
|
|
21
21
|
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
22
22
|
import { getConfig } from "../../config/loader.js";
|
|
23
|
+
import {
|
|
24
|
+
mergeConsecutiveAssistantMessages,
|
|
25
|
+
mergeToolResultsIntoAssistantMessages,
|
|
26
|
+
} from "../../conversations/message-consolidation.js";
|
|
23
27
|
import { createApprovalConversationGenerator } from "../../daemon/approval-generators.js";
|
|
24
28
|
import type { Conversation } from "../../daemon/conversation.js";
|
|
25
29
|
import {
|
|
@@ -121,7 +125,12 @@ import {
|
|
|
121
125
|
resolveTrustContext,
|
|
122
126
|
withSourceChannel,
|
|
123
127
|
} from "../trust-context-resolver.js";
|
|
124
|
-
import {
|
|
128
|
+
import {
|
|
129
|
+
BadRequestError,
|
|
130
|
+
InternalError,
|
|
131
|
+
NotFoundError,
|
|
132
|
+
RouteError,
|
|
133
|
+
} from "./errors.js";
|
|
125
134
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
126
135
|
import { RouteResponse } from "./types.js";
|
|
127
136
|
|
|
@@ -142,6 +151,34 @@ function isValidRiskThreshold(value: unknown): value is RiskThreshold {
|
|
|
142
151
|
);
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Temporary fix — remove when #31994 lands
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
//
|
|
158
|
+
// The canned-response paths in this file (canned greeting, inline approval
|
|
159
|
+
// reply, slash command, /compact, /clean) bypass the agent loop and so don't
|
|
160
|
+
// pick up the per-turn anchor id allocated in conversation-agent-loop.ts.
|
|
161
|
+
// Their `message_complete` events therefore went out without `messageId`,
|
|
162
|
+
// and the macOS client filter at ChatActionHandler.swift:507 dropped those
|
|
163
|
+
// events when they raced past the 50 ms streaming-buffer flush — leaving
|
|
164
|
+
// `isSending` stuck for the full 60 s watchdog window.
|
|
165
|
+
//
|
|
166
|
+
// Centralized so the patch surface is one helper + N one-line callers rather
|
|
167
|
+
// than N duplicated literals. When #31994 lands and stamps these sites with
|
|
168
|
+
// `state.assistantTurnId` directly, grep for `emitCannedMessageComplete` to
|
|
169
|
+
// find every call site and inline-then-delete.
|
|
170
|
+
function emitCannedMessageComplete(
|
|
171
|
+
send: (msg: ServerMessage) => void,
|
|
172
|
+
conversationId: string,
|
|
173
|
+
persistedAssistantId: string,
|
|
174
|
+
): void {
|
|
175
|
+
send({
|
|
176
|
+
type: "message_complete",
|
|
177
|
+
conversationId,
|
|
178
|
+
messageId: persistedAssistantId,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
145
182
|
/**
|
|
146
183
|
* True when a message's persisted metadata explicitly flags it as hidden.
|
|
147
184
|
* Used to suppress internal scaffolding messages from UI history while
|
|
@@ -283,6 +320,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
283
320
|
verifiedActorExternalUserId?: string;
|
|
284
321
|
/** Verified actor principal ID for principal-based authorization. */
|
|
285
322
|
verifiedActorPrincipalId?: string;
|
|
323
|
+
/** Originating client identifier for sync_changed self-echo suppression. */
|
|
324
|
+
originClientId?: string;
|
|
286
325
|
}): Promise<{ consumed: boolean; messageId?: string }> {
|
|
287
326
|
const {
|
|
288
327
|
conversationId,
|
|
@@ -295,6 +334,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
295
334
|
approvalConversationGenerator,
|
|
296
335
|
verifiedActorExternalUserId,
|
|
297
336
|
verifiedActorPrincipalId,
|
|
337
|
+
originClientId,
|
|
298
338
|
} = params;
|
|
299
339
|
const trimmedContent = content.trim();
|
|
300
340
|
|
|
@@ -392,7 +432,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
392
432
|
? "Decision applied."
|
|
393
433
|
: "Request already resolved.");
|
|
394
434
|
const assistantMessage = createAssistantMessage(replyText);
|
|
395
|
-
await addMessage(
|
|
435
|
+
const persistedAssistant = await addMessage(
|
|
396
436
|
conversationId,
|
|
397
437
|
"assistant",
|
|
398
438
|
JSON.stringify(assistantMessage.content),
|
|
@@ -407,9 +447,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
407
447
|
text: replyText,
|
|
408
448
|
conversationId: conversationId,
|
|
409
449
|
});
|
|
410
|
-
onEvent
|
|
450
|
+
emitCannedMessageComplete(onEvent, conversationId, persistedAssistant.id);
|
|
411
451
|
}
|
|
412
|
-
publishConversationMessagesChanged(conversationId);
|
|
452
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
413
453
|
} catch (err) {
|
|
414
454
|
log.warn(
|
|
415
455
|
{ err, conversationId },
|
|
@@ -792,14 +832,8 @@ export function handleListMessages({
|
|
|
792
832
|
// on createdAt. The mismatch is benign — it may return slightly extra
|
|
793
833
|
// data on a page boundary but never loses messages.
|
|
794
834
|
const displayTimestamp = m.sentAt ?? m.timestamp;
|
|
795
|
-
const mergedMessageIds = mergedIdMap.get(m.id) ?? [];
|
|
796
|
-
const daemonMessageId =
|
|
797
|
-
m.role === "assistant"
|
|
798
|
-
? (mergedMessageIds[mergedMessageIds.length - 1] ?? m.id)
|
|
799
|
-
: undefined;
|
|
800
835
|
return {
|
|
801
836
|
id: m.id ?? "",
|
|
802
|
-
...(daemonMessageId ? { daemonMessageId } : {}),
|
|
803
837
|
role: m.role,
|
|
804
838
|
content: m.text,
|
|
805
839
|
timestamp: new Date(displayTimestamp).toISOString(),
|
|
@@ -849,305 +883,6 @@ export function handleListMessages({
|
|
|
849
883
|
return { messages };
|
|
850
884
|
}
|
|
851
885
|
|
|
852
|
-
// ── Tool-result merging ─────────────────────────────────────────────
|
|
853
|
-
|
|
854
|
-
function isToolResultType(type: string): boolean {
|
|
855
|
-
return type === "tool_result" || type === "web_search_tool_result";
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function isSystemNoticeText(block: Record<string, unknown>): boolean {
|
|
859
|
-
if (block.type !== "text") return false;
|
|
860
|
-
const text = typeof block.text === "string" ? block.text : "";
|
|
861
|
-
return (
|
|
862
|
-
text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
/**
|
|
867
|
-
* Merge tool_result blocks from user messages into the preceding assistant
|
|
868
|
-
* message's content array. This lets renderHistoryContent's pendingToolUses
|
|
869
|
-
* map pair tool_use and tool_result blocks, preventing "unknown" tool names.
|
|
870
|
-
*
|
|
871
|
-
* User messages that consist entirely of tool_result blocks (and optional
|
|
872
|
-
* system_notice text) are removed from the output. Mixed messages (tool_result
|
|
873
|
-
* + real user text) keep only the non-tool-result blocks.
|
|
874
|
-
*/
|
|
875
|
-
function mergeToolResultsIntoAssistantMessages(
|
|
876
|
-
messages: MessageRow[],
|
|
877
|
-
): MessageRow[] {
|
|
878
|
-
// Index of the most recent assistant message in the output array.
|
|
879
|
-
let lastAssistantIdx = -1;
|
|
880
|
-
// Parsed content caches — lazily populated per assistant message.
|
|
881
|
-
const parsedAssistantContent = new Map<number, unknown[]>();
|
|
882
|
-
|
|
883
|
-
const result: MessageRow[] = [];
|
|
884
|
-
|
|
885
|
-
for (const msg of messages) {
|
|
886
|
-
if (msg.role === "assistant") {
|
|
887
|
-
lastAssistantIdx = result.length;
|
|
888
|
-
result.push(msg);
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Only process user messages — other roles pass through.
|
|
893
|
-
if (msg.role !== "user") {
|
|
894
|
-
result.push(msg);
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
let blocks: unknown[];
|
|
899
|
-
try {
|
|
900
|
-
const parsed = JSON.parse(msg.content);
|
|
901
|
-
if (!Array.isArray(parsed)) {
|
|
902
|
-
result.push(msg);
|
|
903
|
-
continue;
|
|
904
|
-
}
|
|
905
|
-
blocks = parsed;
|
|
906
|
-
} catch {
|
|
907
|
-
result.push(msg);
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// Separate tool-result blocks from real user content.
|
|
912
|
-
const toolResultBlocks: unknown[] = [];
|
|
913
|
-
const otherBlocks: unknown[] = [];
|
|
914
|
-
for (const block of blocks) {
|
|
915
|
-
if (
|
|
916
|
-
typeof block === "object" &&
|
|
917
|
-
block !== null &&
|
|
918
|
-
typeof (block as Record<string, unknown>).type === "string"
|
|
919
|
-
) {
|
|
920
|
-
const rec = block as Record<string, unknown>;
|
|
921
|
-
if (isToolResultType(rec.type as string)) {
|
|
922
|
-
toolResultBlocks.push(block);
|
|
923
|
-
} else if (isSystemNoticeText(rec)) {
|
|
924
|
-
// System notices don't count as user content — drop them when
|
|
925
|
-
// the message is otherwise tool-result-only.
|
|
926
|
-
otherBlocks.push(block);
|
|
927
|
-
} else {
|
|
928
|
-
otherBlocks.push(block);
|
|
929
|
-
}
|
|
930
|
-
} else {
|
|
931
|
-
otherBlocks.push(block);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// No tool results → pass through unchanged. System notices are only
|
|
936
|
-
// injected alongside tool results in the agent loop, so a pure user
|
|
937
|
-
// message (no tool_result blocks) should never be filtered — even if
|
|
938
|
-
// the user's text happens to look like a system_notice tag.
|
|
939
|
-
if (toolResultBlocks.length === 0) {
|
|
940
|
-
result.push(msg);
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// Append tool_result blocks to the preceding assistant message's content.
|
|
945
|
-
if (lastAssistantIdx >= 0) {
|
|
946
|
-
const assistant = result[lastAssistantIdx];
|
|
947
|
-
let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
|
|
948
|
-
if (!assistantContent) {
|
|
949
|
-
try {
|
|
950
|
-
const parsed = JSON.parse(assistant.content);
|
|
951
|
-
assistantContent = Array.isArray(parsed) ? parsed : [parsed];
|
|
952
|
-
} catch {
|
|
953
|
-
assistantContent = [];
|
|
954
|
-
}
|
|
955
|
-
parsedAssistantContent.set(lastAssistantIdx, assistantContent);
|
|
956
|
-
}
|
|
957
|
-
assistantContent.push(...toolResultBlocks);
|
|
958
|
-
} else {
|
|
959
|
-
// No preceding assistant message (pagination boundary) — keep the
|
|
960
|
-
// original message as-is to avoid permanent data loss. The preceding
|
|
961
|
-
// assistant tool_use lives in the previous page; dropping the result
|
|
962
|
-
// here would be unrecoverable.
|
|
963
|
-
// Still strip system notices so internal prompt text isn't exposed.
|
|
964
|
-
const filteredBlocks = blocks.filter(
|
|
965
|
-
(b) =>
|
|
966
|
-
!(
|
|
967
|
-
typeof b === "object" &&
|
|
968
|
-
b !== null &&
|
|
969
|
-
isSystemNoticeText(b as Record<string, unknown>)
|
|
970
|
-
),
|
|
971
|
-
);
|
|
972
|
-
result.push({
|
|
973
|
-
...msg,
|
|
974
|
-
content:
|
|
975
|
-
filteredBlocks.length === blocks.length
|
|
976
|
-
? msg.content
|
|
977
|
-
: JSON.stringify(filteredBlocks),
|
|
978
|
-
});
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// If the user message had only tool_result (+ system_notice) blocks,
|
|
983
|
-
// suppress it entirely. Otherwise keep the non-tool-result content.
|
|
984
|
-
const realUserContent = otherBlocks.filter(
|
|
985
|
-
(b) =>
|
|
986
|
-
!(
|
|
987
|
-
typeof b === "object" &&
|
|
988
|
-
b !== null &&
|
|
989
|
-
isSystemNoticeText(b as Record<string, unknown>)
|
|
990
|
-
),
|
|
991
|
-
);
|
|
992
|
-
if (realUserContent.length > 0) {
|
|
993
|
-
result.push({ ...msg, content: JSON.stringify(otherBlocks) });
|
|
994
|
-
}
|
|
995
|
-
// else: tool-result-only → suppressed (results already merged above)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Write back any modified assistant message content.
|
|
999
|
-
for (const [idx, content] of parsedAssistantContent) {
|
|
1000
|
-
result[idx] = { ...result[idx], content: JSON.stringify(content) };
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
return result;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// ── Consecutive assistant message merging ────────────────────────────
|
|
1007
|
-
|
|
1008
|
-
/** Parse a message's JSON content into an array of content blocks. */
|
|
1009
|
-
function parseContentBlocks(content: string): unknown[] {
|
|
1010
|
-
try {
|
|
1011
|
-
const parsed = JSON.parse(content);
|
|
1012
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
1013
|
-
} catch (err) {
|
|
1014
|
-
log.warn(
|
|
1015
|
-
{ err },
|
|
1016
|
-
"Failed to parse content blocks during assistant message merge",
|
|
1017
|
-
);
|
|
1018
|
-
return [];
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
/**
|
|
1023
|
-
* Append content blocks from a donor message onto a target block array.
|
|
1024
|
-
* Parses the donor's JSON content and pushes each block into `target`.
|
|
1025
|
-
*/
|
|
1026
|
-
function appendContentBlocks(target: unknown[], donorContent: string): void {
|
|
1027
|
-
try {
|
|
1028
|
-
const parsed = JSON.parse(donorContent);
|
|
1029
|
-
if (Array.isArray(parsed)) {
|
|
1030
|
-
target.push(...parsed);
|
|
1031
|
-
} else {
|
|
1032
|
-
target.push(parsed);
|
|
1033
|
-
}
|
|
1034
|
-
} catch (err) {
|
|
1035
|
-
log.warn(
|
|
1036
|
-
{ err },
|
|
1037
|
-
"Failed to parse donor content blocks during assistant message merge",
|
|
1038
|
-
);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Promote metadata fields from a donor message to the surviving message
|
|
1044
|
-
* when the survivor lacks them. Currently promotes `subagentNotification`.
|
|
1045
|
-
* Returns a new MessageRow if promotion occurred, otherwise the original.
|
|
1046
|
-
*/
|
|
1047
|
-
function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
|
|
1048
|
-
if (donor.metadata && survivor.metadata) {
|
|
1049
|
-
try {
|
|
1050
|
-
const survivorMeta = JSON.parse(survivor.metadata);
|
|
1051
|
-
const donorMeta = JSON.parse(donor.metadata);
|
|
1052
|
-
if (
|
|
1053
|
-
!survivorMeta.subagentNotification &&
|
|
1054
|
-
donorMeta.subagentNotification
|
|
1055
|
-
) {
|
|
1056
|
-
survivorMeta.subagentNotification = donorMeta.subagentNotification;
|
|
1057
|
-
return { ...survivor, metadata: JSON.stringify(survivorMeta) };
|
|
1058
|
-
}
|
|
1059
|
-
} catch (err) {
|
|
1060
|
-
log.warn(
|
|
1061
|
-
{ err },
|
|
1062
|
-
"Failed to parse metadata during assistant message merge",
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
} else if (donor.metadata && !survivor.metadata) {
|
|
1066
|
-
return { ...survivor, metadata: donor.metadata };
|
|
1067
|
-
}
|
|
1068
|
-
return survivor;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Merge consecutive assistant messages into a single message at query time.
|
|
1073
|
-
*
|
|
1074
|
-
* During streaming, all assistant turns within one agent loop accumulate on
|
|
1075
|
-
* a single client-side ChatMessage. In the DB, each API turn is stored as a
|
|
1076
|
-
* separate assistant row (consolidation is deferred to compaction for
|
|
1077
|
-
* prefix-cache stability). This produces N separate assistant messages that
|
|
1078
|
-
* the client renders as N individual bubbles — each showing "Completed 1
|
|
1079
|
-
* step" instead of one grouped "Completed N steps" accordion.
|
|
1080
|
-
*
|
|
1081
|
-
* This function concatenates the content block arrays of consecutive
|
|
1082
|
-
* assistant messages (no intervening user messages after tool-result
|
|
1083
|
-
* merging) into the first message of each run. The merged messages are
|
|
1084
|
-
* removed from the output. This is query-time only — the DB is not
|
|
1085
|
-
* modified.
|
|
1086
|
-
*
|
|
1087
|
-
* The first message in each run keeps its id, createdAt, and metadata so
|
|
1088
|
-
* that attachment lookups, display timestamps, and subagent notifications
|
|
1089
|
-
* continue to work. Metadata from later messages in the run (e.g.
|
|
1090
|
-
* subagentNotification) is preserved by promoting it to the surviving
|
|
1091
|
-
* message when the surviving message has no metadata of its own for that
|
|
1092
|
-
* field.
|
|
1093
|
-
*/
|
|
1094
|
-
function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
|
|
1095
|
-
messages: MessageRow[];
|
|
1096
|
-
/** Maps each surviving message ID → all original message IDs merged into it. */
|
|
1097
|
-
mergedIdMap: Map<string, string[]>;
|
|
1098
|
-
} {
|
|
1099
|
-
const result: MessageRow[] = [];
|
|
1100
|
-
// Key = index in `result`, value = accumulated content blocks.
|
|
1101
|
-
const pendingMerges = new Map<number, unknown[]>();
|
|
1102
|
-
// Key = index in `result`, value = IDs of messages merged into the target.
|
|
1103
|
-
const mergedIds = new Map<number, string[]>();
|
|
1104
|
-
|
|
1105
|
-
for (const msg of messages) {
|
|
1106
|
-
const lastIdx = result.length - 1;
|
|
1107
|
-
const isConsecutiveAssistant =
|
|
1108
|
-
msg.role === "assistant" &&
|
|
1109
|
-
lastIdx >= 0 &&
|
|
1110
|
-
result[lastIdx].role === "assistant";
|
|
1111
|
-
|
|
1112
|
-
if (!isConsecutiveAssistant) {
|
|
1113
|
-
result.push(msg);
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// Track the donor message ID.
|
|
1118
|
-
let ids = mergedIds.get(lastIdx);
|
|
1119
|
-
if (!ids) {
|
|
1120
|
-
ids = [];
|
|
1121
|
-
mergedIds.set(lastIdx, ids);
|
|
1122
|
-
}
|
|
1123
|
-
ids.push(msg.id);
|
|
1124
|
-
|
|
1125
|
-
// Lazily parse the target's content on first merge.
|
|
1126
|
-
let targetContent = pendingMerges.get(lastIdx);
|
|
1127
|
-
if (!targetContent) {
|
|
1128
|
-
targetContent = parseContentBlocks(result[lastIdx].content);
|
|
1129
|
-
pendingMerges.set(lastIdx, targetContent);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
appendContentBlocks(targetContent, msg.content);
|
|
1133
|
-
result[lastIdx] = promoteMetadata(result[lastIdx], msg);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// Write back merged content for any messages that were targets.
|
|
1137
|
-
for (const [idx, content] of pendingMerges) {
|
|
1138
|
-
result[idx] = { ...result[idx], content: JSON.stringify(content) };
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// Build the merged ID map keyed by surviving message ID.
|
|
1142
|
-
const mergedIdMap = new Map<string, string[]>();
|
|
1143
|
-
for (const [idx, ids] of mergedIds) {
|
|
1144
|
-
mergedIdMap.set(result[idx].id, ids);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
return { messages: result, mergedIdMap };
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/**
|
|
1151
886
|
/**
|
|
1152
887
|
* Persist the pre-chat onboarding payload to disk.
|
|
1153
888
|
*
|
|
@@ -1240,6 +975,7 @@ export async function handleSendMessage(
|
|
|
1240
975
|
): Promise<unknown> {
|
|
1241
976
|
const body = (rawBody ?? {}) as {
|
|
1242
977
|
conversationKey?: string;
|
|
978
|
+
conversationId?: string;
|
|
1243
979
|
content?: string;
|
|
1244
980
|
attachmentIds?: string[];
|
|
1245
981
|
sourceChannel?: string;
|
|
@@ -1271,8 +1007,14 @@ export async function handleSendMessage(
|
|
|
1271
1007
|
|
|
1272
1008
|
const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
|
|
1273
1009
|
const principalType = headers?.["x-vellum-principal-type"];
|
|
1010
|
+
const originClientId =
|
|
1011
|
+
headers?.["x-vellum-client-id"]?.trim() || undefined;
|
|
1274
1012
|
|
|
1275
1013
|
const { conversationKey, content, attachmentIds } = body;
|
|
1014
|
+
const inboundConversationId =
|
|
1015
|
+
typeof body.conversationId === "string" && body.conversationId.length > 0
|
|
1016
|
+
? body.conversationId
|
|
1017
|
+
: undefined;
|
|
1276
1018
|
const clientMessageId =
|
|
1277
1019
|
typeof body.clientMessageId === "string" ? body.clientMessageId : undefined;
|
|
1278
1020
|
const requestedInferenceProfile =
|
|
@@ -1340,12 +1082,6 @@ export async function handleSendMessage(
|
|
|
1340
1082
|
? (canonicalizeTimeZone(body.clientTimezone) ?? undefined)
|
|
1341
1083
|
: undefined;
|
|
1342
1084
|
|
|
1343
|
-
// When conversationKey is omitted, derive a stable default from
|
|
1344
|
-
// sourceChannel + sourceInterface so that repeated calls from the same
|
|
1345
|
-
// channel/interface pair share a single conversation thread.
|
|
1346
|
-
const resolvedConversationKey =
|
|
1347
|
-
conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
|
|
1348
|
-
|
|
1349
1085
|
// Reject non-string content values (numbers, objects, etc.)
|
|
1350
1086
|
if (content != null && typeof content !== "string") {
|
|
1351
1087
|
throw new BadRequestError("content must be a string");
|
|
@@ -1409,9 +1145,40 @@ export async function handleSendMessage(
|
|
|
1409
1145
|
// timer so the next heartbeat is a full interval after this interaction.
|
|
1410
1146
|
HeartbeatService.getInstance()?.resetTimer();
|
|
1411
1147
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1148
|
+
// Resolve the target conversation. Fetch by `conversationId` (the
|
|
1149
|
+
// assistant-minted internal id) when the client supplies it — clients
|
|
1150
|
+
// must obtain this id from a prior daemon response, so a missing row
|
|
1151
|
+
// is a 404. Otherwise fall through to the external-key path: the
|
|
1152
|
+
// client-supplied `conversationKey` (used by non-vellum channels and
|
|
1153
|
+
// the web idempotency flow) or, when neither is provided, a stable
|
|
1154
|
+
// default keyed on sourceChannel + sourceInterface so repeated calls
|
|
1155
|
+
// from the same channel/interface share a single thread.
|
|
1156
|
+
let mapping: {
|
|
1157
|
+
conversationId: string;
|
|
1158
|
+
conversationType: string;
|
|
1159
|
+
created: boolean;
|
|
1160
|
+
};
|
|
1161
|
+
if (inboundConversationId !== undefined) {
|
|
1162
|
+
const existing = getConversation(inboundConversationId);
|
|
1163
|
+
if (!existing) {
|
|
1164
|
+
throw new NotFoundError(
|
|
1165
|
+
`Conversation ${inboundConversationId} not found`,
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
mapping = {
|
|
1169
|
+
conversationId: existing.id,
|
|
1170
|
+
conversationType: existing.conversationType,
|
|
1171
|
+
created: false,
|
|
1172
|
+
};
|
|
1173
|
+
} else {
|
|
1174
|
+
const resolvedConversationKey =
|
|
1175
|
+
conversationKey && conversationKey.length > 0
|
|
1176
|
+
? conversationKey
|
|
1177
|
+
: `default:${sourceChannel}:${sourceInterface}`;
|
|
1178
|
+
mapping = getOrCreateConversation(resolvedConversationKey, {
|
|
1179
|
+
conversationType: "standard",
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1415
1182
|
|
|
1416
1183
|
if (requestedRiskThreshold !== undefined) {
|
|
1417
1184
|
const result = await ipcCall("set_conversation_threshold", {
|
|
@@ -1445,6 +1212,7 @@ export async function handleSendMessage(
|
|
|
1445
1212
|
publishConversationListAndMetadataChanged(
|
|
1446
1213
|
"created",
|
|
1447
1214
|
mapping.conversationId,
|
|
1215
|
+
originClientId,
|
|
1448
1216
|
);
|
|
1449
1217
|
}
|
|
1450
1218
|
}
|
|
@@ -1664,7 +1432,7 @@ export async function handleSendMessage(
|
|
|
1664
1432
|
const conversationId = mapping.conversationId;
|
|
1665
1433
|
|
|
1666
1434
|
const assistantMsg = createAssistantMessage(cannedGreeting);
|
|
1667
|
-
await addMessage(
|
|
1435
|
+
const persistedAssistant = await addMessage(
|
|
1668
1436
|
mapping.conversationId,
|
|
1669
1437
|
"assistant",
|
|
1670
1438
|
JSON.stringify(assistantMsg.content),
|
|
@@ -1708,8 +1476,12 @@ export async function handleSendMessage(
|
|
|
1708
1476
|
text: cannedGreeting,
|
|
1709
1477
|
conversationId,
|
|
1710
1478
|
});
|
|
1711
|
-
|
|
1712
|
-
|
|
1479
|
+
emitCannedMessageComplete(
|
|
1480
|
+
broadcastMessage,
|
|
1481
|
+
conversationId,
|
|
1482
|
+
persistedAssistant.id,
|
|
1483
|
+
);
|
|
1484
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1713
1485
|
conversation.processing = false;
|
|
1714
1486
|
silentlyWithLog(
|
|
1715
1487
|
conversation.drainQueue(),
|
|
@@ -1787,6 +1559,7 @@ export async function handleSendMessage(
|
|
|
1787
1559
|
: deps.approvalConversationGenerator,
|
|
1788
1560
|
verifiedActorExternalUserId,
|
|
1789
1561
|
verifiedActorPrincipalId,
|
|
1562
|
+
originClientId,
|
|
1790
1563
|
});
|
|
1791
1564
|
if (inlineReplyResult.consumed) {
|
|
1792
1565
|
return {
|
|
@@ -1975,7 +1748,7 @@ export async function handleSendMessage(
|
|
|
1975
1748
|
conversation.getMessages().push(llmMsg);
|
|
1976
1749
|
|
|
1977
1750
|
const assistantMsg = createAssistantMessage(slashResult.message);
|
|
1978
|
-
await addMessage(
|
|
1751
|
+
const persistedAssistant = await addMessage(
|
|
1979
1752
|
mapping.conversationId,
|
|
1980
1753
|
"assistant",
|
|
1981
1754
|
JSON.stringify(assistantMsg.content),
|
|
@@ -2030,11 +1803,12 @@ export async function handleSendMessage(
|
|
|
2030
1803
|
text: message,
|
|
2031
1804
|
conversationId,
|
|
2032
1805
|
});
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
conversationId
|
|
2036
|
-
|
|
2037
|
-
|
|
1806
|
+
emitCannedMessageComplete(
|
|
1807
|
+
broadcastMessage,
|
|
1808
|
+
conversationId,
|
|
1809
|
+
persistedAssistant.id,
|
|
1810
|
+
);
|
|
1811
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
2038
1812
|
conversation.processing = false;
|
|
2039
1813
|
silentlyWithLog(conversation.drainQueue(), "slash-command queue drain");
|
|
2040
1814
|
}, 0);
|
|
@@ -2062,12 +1836,22 @@ export async function handleSendMessage(
|
|
|
2062
1836
|
assistantMessageInterface: sourceInterface,
|
|
2063
1837
|
};
|
|
2064
1838
|
const cleanMsg = createUserMessage(rawContent, attachments);
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
1839
|
+
let persisted: Awaited<ReturnType<typeof addMessage>>;
|
|
1840
|
+
try {
|
|
1841
|
+
persisted = await addMessage(
|
|
1842
|
+
mapping.conversationId,
|
|
1843
|
+
"user",
|
|
1844
|
+
JSON.stringify(cleanMsg.content),
|
|
1845
|
+
channelMeta,
|
|
1846
|
+
);
|
|
1847
|
+
} catch (err) {
|
|
1848
|
+
// The fire-and-forget compaction below owns clearing `processing`, but a
|
|
1849
|
+
// throw from this initial persist never reaches it — reset here so the
|
|
1850
|
+
// conversation isn't stranded in queued mode.
|
|
1851
|
+
conversation.processing = false;
|
|
1852
|
+
silentlyWithLog(conversation.drainQueue(), "compact-command queue drain");
|
|
1853
|
+
throw err;
|
|
1854
|
+
}
|
|
2071
1855
|
conversation.getMessages().push(cleanMsg);
|
|
2072
1856
|
|
|
2073
1857
|
const conversationId = mapping.conversationId;
|
|
@@ -2085,7 +1869,7 @@ export async function handleSendMessage(
|
|
|
2085
1869
|
messageId: persisted.id,
|
|
2086
1870
|
clientMessageId,
|
|
2087
1871
|
});
|
|
2088
|
-
publishConversationMessagesChanged(conversationId);
|
|
1872
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
2089
1873
|
conversation.emitActivityState(
|
|
2090
1874
|
"thinking",
|
|
2091
1875
|
"context_compacting",
|
|
@@ -2097,7 +1881,7 @@ export async function handleSendMessage(
|
|
|
2097
1881
|
const responseText = formatCompactResult(result);
|
|
2098
1882
|
|
|
2099
1883
|
const assistantMsg = createAssistantMessage(responseText);
|
|
2100
|
-
await addMessage(
|
|
1884
|
+
const persistedAssistant = await addMessage(
|
|
2101
1885
|
conversationId,
|
|
2102
1886
|
"assistant",
|
|
2103
1887
|
JSON.stringify(assistantMsg.content),
|
|
@@ -2111,11 +1895,15 @@ export async function handleSendMessage(
|
|
|
2111
1895
|
text: responseText,
|
|
2112
1896
|
conversationId,
|
|
2113
1897
|
});
|
|
2114
|
-
|
|
2115
|
-
|
|
1898
|
+
emitCannedMessageComplete(
|
|
1899
|
+
broadcastMessage,
|
|
1900
|
+
conversationId,
|
|
1901
|
+
persistedAssistant.id,
|
|
1902
|
+
);
|
|
1903
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
2116
1904
|
} catch (err) {
|
|
2117
1905
|
if (assistantMessagePersisted) {
|
|
2118
|
-
publishConversationMessagesChanged(conversationId);
|
|
1906
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
2119
1907
|
}
|
|
2120
1908
|
log.error({ err, conversationId }, "Compact command failed");
|
|
2121
1909
|
broadcastMessage({
|
|
@@ -2143,78 +1931,87 @@ export async function handleSendMessage(
|
|
|
2143
1931
|
|
|
2144
1932
|
if (slashResult.kind === "clean") {
|
|
2145
1933
|
conversation.processing = true;
|
|
2146
|
-
const provenance = provenanceFromTrustContext(conversation.trustContext);
|
|
2147
|
-
const channelMeta = {
|
|
2148
|
-
...provenance,
|
|
2149
|
-
userMessageChannel: sourceChannel,
|
|
2150
|
-
assistantMessageChannel: sourceChannel,
|
|
2151
|
-
userMessageInterface: sourceInterface,
|
|
2152
|
-
assistantMessageInterface: sourceInterface,
|
|
2153
|
-
};
|
|
2154
|
-
const cleanMsg = createUserMessage(rawContent, attachments);
|
|
2155
|
-
const persisted = await addMessage(
|
|
2156
|
-
mapping.conversationId,
|
|
2157
|
-
"user",
|
|
2158
|
-
JSON.stringify(cleanMsg.content),
|
|
2159
|
-
channelMeta,
|
|
2160
|
-
);
|
|
2161
|
-
conversation.getMessages().push(cleanMsg);
|
|
2162
|
-
|
|
2163
1934
|
const conversationId = mapping.conversationId;
|
|
2164
|
-
|
|
2165
|
-
|
|
1935
|
+
// Outer try/finally guarantees the processing flag is cleared (and the
|
|
1936
|
+
// queue drained) on every failure path — including a throw from the
|
|
1937
|
+
// initial user-message persist below, which would otherwise leave the
|
|
1938
|
+
// conversation stuck in queued mode indefinitely.
|
|
2166
1939
|
try {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
await addMessage(
|
|
2181
|
-
conversationId,
|
|
2182
|
-
"assistant",
|
|
2183
|
-
JSON.stringify(assistantMsg.content),
|
|
1940
|
+
const provenance = provenanceFromTrustContext(conversation.trustContext);
|
|
1941
|
+
const channelMeta = {
|
|
1942
|
+
...provenance,
|
|
1943
|
+
userMessageChannel: sourceChannel,
|
|
1944
|
+
assistantMessageChannel: sourceChannel,
|
|
1945
|
+
userMessageInterface: sourceInterface,
|
|
1946
|
+
assistantMessageInterface: sourceInterface,
|
|
1947
|
+
};
|
|
1948
|
+
const cleanMsg = createUserMessage(rawContent, attachments);
|
|
1949
|
+
const persisted = await addMessage(
|
|
1950
|
+
mapping.conversationId,
|
|
1951
|
+
"user",
|
|
1952
|
+
JSON.stringify(cleanMsg.content),
|
|
2184
1953
|
channelMeta,
|
|
2185
1954
|
);
|
|
2186
|
-
|
|
2187
|
-
conversation.getMessages().push(assistantMsg);
|
|
1955
|
+
conversation.getMessages().push(cleanMsg);
|
|
2188
1956
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
publishConversationMessagesChanged(conversationId);
|
|
1957
|
+
let assistantMessagePersisted = false;
|
|
1958
|
+
try {
|
|
1959
|
+
broadcastMessage({
|
|
1960
|
+
type: "user_message_echo",
|
|
1961
|
+
text: rawContent,
|
|
1962
|
+
conversationId,
|
|
1963
|
+
messageId: persisted.id,
|
|
1964
|
+
clientMessageId,
|
|
1965
|
+
});
|
|
1966
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1967
|
+
|
|
1968
|
+
const result = await conversation.forceClean();
|
|
1969
|
+
const responseText = formatCleanResult(result);
|
|
1970
|
+
|
|
1971
|
+
const assistantMsg = createAssistantMessage(responseText);
|
|
1972
|
+
const persistedAssistant = await addMessage(
|
|
1973
|
+
conversationId,
|
|
1974
|
+
"assistant",
|
|
1975
|
+
JSON.stringify(assistantMsg.content),
|
|
1976
|
+
channelMeta,
|
|
1977
|
+
);
|
|
1978
|
+
assistantMessagePersisted = true;
|
|
1979
|
+
conversation.getMessages().push(assistantMsg);
|
|
1980
|
+
|
|
1981
|
+
broadcastMessage({
|
|
1982
|
+
type: "assistant_text_delta",
|
|
1983
|
+
text: responseText,
|
|
1984
|
+
conversationId,
|
|
1985
|
+
});
|
|
1986
|
+
emitCannedMessageComplete(
|
|
1987
|
+
broadcastMessage,
|
|
1988
|
+
conversationId,
|
|
1989
|
+
persistedAssistant.id,
|
|
1990
|
+
);
|
|
1991
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
if (assistantMessagePersisted) {
|
|
1994
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1995
|
+
}
|
|
1996
|
+
log.error({ err, conversationId }, "Clean command failed");
|
|
1997
|
+
broadcastMessage({
|
|
1998
|
+
type: "conversation_error",
|
|
1999
|
+
conversationId,
|
|
2000
|
+
code: "UNKNOWN",
|
|
2001
|
+
userMessage: `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2002
|
+
retryable: true,
|
|
2003
|
+
});
|
|
2199
2004
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2005
|
+
|
|
2006
|
+
return {
|
|
2007
|
+
accepted: true,
|
|
2008
|
+
messageId: persisted.id,
|
|
2203
2009
|
conversationId,
|
|
2204
|
-
|
|
2205
|
-
userMessage: `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2206
|
-
retryable: true,
|
|
2207
|
-
});
|
|
2010
|
+
};
|
|
2208
2011
|
} finally {
|
|
2209
2012
|
conversation.processing = false;
|
|
2210
2013
|
silentlyWithLog(conversation.drainQueue(), "clean-command queue drain");
|
|
2211
2014
|
}
|
|
2212
|
-
|
|
2213
|
-
return {
|
|
2214
|
-
accepted: true,
|
|
2215
|
-
messageId: persisted.id,
|
|
2216
|
-
conversationId,
|
|
2217
|
-
};
|
|
2218
2015
|
}
|
|
2219
2016
|
|
|
2220
2017
|
const resolvedContent = slashResult.content;
|
|
@@ -2240,7 +2037,7 @@ export async function handleSendMessage(
|
|
|
2240
2037
|
requestId,
|
|
2241
2038
|
clientMessageId,
|
|
2242
2039
|
});
|
|
2243
|
-
publishConversationMessagesChanged(mapping.conversationId);
|
|
2040
|
+
publishConversationMessagesChanged(mapping.conversationId, originClientId);
|
|
2244
2041
|
|
|
2245
2042
|
// Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
|
|
2246
2043
|
conversation
|
|
@@ -2285,14 +2082,25 @@ async function generateLlmSuggestion(
|
|
|
2285
2082
|
? escapeXmlContent(priorUserText)
|
|
2286
2083
|
: priorUserText;
|
|
2287
2084
|
|
|
2288
|
-
const systemPrompt =
|
|
2289
|
-
"You generate short, casual reply suggestions a user might type next in a chat.
|
|
2085
|
+
const systemPrompt = [
|
|
2086
|
+
"You generate short, casual reply suggestions a user might type next in a chat.",
|
|
2087
|
+
"Match the tone and register of the preceding conversation.",
|
|
2088
|
+
"",
|
|
2089
|
+
"CRITICAL — write from the USER'S perspective only, NEVER from the assistant's:",
|
|
2090
|
+
"- The suggestion is what the USER will type into the chat input",
|
|
2091
|
+
"- Use first-person \"I\" only if the user has used it in their prior messages",
|
|
2092
|
+
"- NEVER start with phrases like \"I can help\", \"Here's what\", \"Let me\", \"I'd suggest\" — those are assistant-voice",
|
|
2093
|
+
"- Think: if you were the user reading the assistant's reply, what question or follow-up would you ask next?",
|
|
2094
|
+
"",
|
|
2095
|
+
"Output only the reply text inside the requested tags — no preamble, no commentary.",
|
|
2096
|
+
].join("\n");
|
|
2290
2097
|
|
|
2291
2098
|
const userPrompt =
|
|
2292
2099
|
`Here is the end of a conversation:\n\n` +
|
|
2293
2100
|
`<user_message>${truncatedUser ?? "(no prior user message)"}</user_message>\n` +
|
|
2294
2101
|
`<assistant_message>${truncatedAssistant}</assistant_message>\n\n` +
|
|
2295
|
-
`Write the
|
|
2102
|
+
`Write the USER'S next reply — what the user would type. Focus on the LAST question or call-to-action in the assistant message. Keep it short (under 15 words), casual, and in the user's voice. ` +
|
|
2103
|
+
`The reply must read as something typed BY the user, not something the assistant would say. Respond in this exact format:\n\n` +
|
|
2296
2104
|
`<reply>YOUR_REPLY_HERE</reply>`;
|
|
2297
2105
|
|
|
2298
2106
|
// Single user message only — no assistant-role prefill. Anthropic
|
|
@@ -2368,14 +2176,27 @@ export async function handleGetSuggestion(
|
|
|
2368
2176
|
};
|
|
2369
2177
|
|
|
2370
2178
|
const conversationKey = queryParams?.conversationKey;
|
|
2371
|
-
|
|
2372
|
-
|
|
2179
|
+
const conversationId = queryParams?.conversationId;
|
|
2180
|
+
if (!conversationKey && !conversationId) {
|
|
2181
|
+
throw new BadRequestError(
|
|
2182
|
+
"conversationKey or conversationId query parameter is required",
|
|
2183
|
+
);
|
|
2373
2184
|
}
|
|
2374
2185
|
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2186
|
+
let resolvedConversationId: string | undefined;
|
|
2187
|
+
if (conversationId) {
|
|
2188
|
+
resolvedConversationId = conversationId;
|
|
2189
|
+
} else if (conversationKey) {
|
|
2190
|
+
const mapping = getConversationByKey(conversationKey);
|
|
2191
|
+
if (mapping) {
|
|
2192
|
+
resolvedConversationId = mapping.conversationId;
|
|
2193
|
+
} else if (getConversation(conversationKey)) {
|
|
2194
|
+
resolvedConversationId = conversationKey;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (!resolvedConversationId) return noSuggestion;
|
|
2377
2198
|
|
|
2378
|
-
const rawMessages = getMessages(
|
|
2199
|
+
const rawMessages = getMessages(resolvedConversationId);
|
|
2379
2200
|
if (rawMessages.length === 0) return noSuggestion;
|
|
2380
2201
|
|
|
2381
2202
|
// Staleness check: compare requested messageId against the latest
|
|
@@ -2629,10 +2450,31 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2629
2450
|
description:
|
|
2630
2451
|
"Return an LLM-generated follow-up suggestion for the most recent assistant message.",
|
|
2631
2452
|
tags: ["messages"],
|
|
2453
|
+
queryParams: [
|
|
2454
|
+
{
|
|
2455
|
+
name: "conversationId",
|
|
2456
|
+
type: "string",
|
|
2457
|
+
description:
|
|
2458
|
+
"Conversation ID to fetch a suggestion for. Either this or conversationKey is required.",
|
|
2459
|
+
},
|
|
2460
|
+
{
|
|
2461
|
+
name: "conversationKey",
|
|
2462
|
+
type: "string",
|
|
2463
|
+
description:
|
|
2464
|
+
"Legacy conversation key. Either this or conversationId is required.",
|
|
2465
|
+
},
|
|
2466
|
+
{
|
|
2467
|
+
name: "messageId",
|
|
2468
|
+
type: "string",
|
|
2469
|
+
description:
|
|
2470
|
+
"Optional. Latest assistant message ID the client has seen — used to detect staleness.",
|
|
2471
|
+
},
|
|
2472
|
+
],
|
|
2632
2473
|
responseBody: z.object({
|
|
2633
|
-
suggestion: z.string(),
|
|
2634
|
-
messageId: z.string(),
|
|
2474
|
+
suggestion: z.string().nullable(),
|
|
2475
|
+
messageId: z.string().nullable(),
|
|
2635
2476
|
source: z.string(),
|
|
2477
|
+
stale: z.boolean().optional(),
|
|
2636
2478
|
}),
|
|
2637
2479
|
handler: async (args) =>
|
|
2638
2480
|
handleGetSuggestion(args, {
|