@vellumai/assistant 0.8.3 → 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/docker-entrypoint.sh +0 -1
- package/docs/browser-use-architecture-phase2.md +1 -1
- package/knip.json +2 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +1492 -100
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +302 -33
- package/src/__tests__/approval-cascade.test.ts +1 -1
- 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 +4 -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-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-guardian.test.ts +3 -3
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/checker.test.ts +6 -15
- package/src/__tests__/compaction-events.test.ts +2 -1
- package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -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__/config-watcher.test.ts +1 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +55 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +228 -8
- package/src/__tests__/conversation-agent-loop.test.ts +188 -129
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-clear-safety.test.ts +25 -25
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- 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 +324 -0
- package/src/__tests__/conversation-lifecycle.test.ts +53 -12
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-load-history-stripped.test.ts +279 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- 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-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-skill-tools.test.ts +2 -5
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-store.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-sync-tags.test.ts +99 -32
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -1
- 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-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 +7 -0
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/dynamic-page-surface.test.ts +2 -2
- package/src/__tests__/email-html-renderer.test.ts +12 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- 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__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
- package/src/__tests__/heartbeat-service.test.ts +4 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +77 -17
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- 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 +161 -9
- package/src/__tests__/llm-usage-store.test.ts +66 -0
- package/src/__tests__/log-export-routes.test.ts +99 -2
- 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__/message-queue-steer.test.ts +114 -0
- 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 +151 -0
- package/src/__tests__/openai-responses-provider.test.ts +118 -16
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +189 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
- package/src/__tests__/platform.test.ts +2 -5
- package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
- package/src/__tests__/plugin-bootstrap.test.ts +2 -2
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- 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__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- 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__/server-history-render.test.ts +83 -4
- 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__/steer-tool-repair.test.ts +249 -0
- 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 +161 -124
- package/src/__tests__/terminal-tools.test.ts +12 -2
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- 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 +89 -53
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- 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__/twilio-routes.test.ts +1 -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__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +94 -10
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- 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/attachments.ts +1 -0
- package/src/agent/loop.ts +65 -20
- 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/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/background-wake/runtime-registry.ts +24 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/browser.test.ts +23 -5
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- 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 +10 -12
- package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
- package/src/cli/commands/browser.ts +247 -0
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/domain.ts +91 -41
- package/src/cli/commands/inference-providers.ts +147 -1
- 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 +483 -0
- package/src/cli/commands/memory-v3-render.ts +344 -0
- package/src/cli/commands/memory-v3.ts +316 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/program.ts +2 -0
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/assistant-feature-flags.ts +21 -9
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +124 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +258 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.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/SKILL.md +8 -0
- 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-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +24 -12
- package/src/config/call-site-defaults.ts +20 -0
- package/src/config/feature-flag-registry.json +115 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +217 -1
- package/src/config/schemas/call-site-catalog.ts +35 -0
- package/src/config/schemas/llm.ts +14 -0
- package/src/config/schemas/memory-v2.ts +294 -1
- package/src/config/schemas/memory.ts +2 -1
- package/src/context/compactor.ts +60 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
- package/src/conversations/message-consolidation.ts +404 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +155 -36
- package/src/daemon/conversation-agent-loop.ts +307 -88
- package/src/daemon/conversation-error.ts +31 -1
- package/src/daemon/conversation-lifecycle.ts +149 -118
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +145 -84
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +70 -3
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +54 -32
- package/src/daemon/disk-pressure-guard.ts +14 -2
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +2 -0
- package/src/daemon/handlers/conversations.ts +90 -3
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +5 -5
- package/src/daemon/host-cu-proxy.ts +5 -5
- package/src/daemon/host-file-proxy.ts +5 -5
- package/src/daemon/host-proxy-base.ts +4 -4
- package/src/daemon/host-transfer-proxy.ts +11 -11
- package/src/daemon/lifecycle.ts +40 -23
- package/src/daemon/meet-manifest-loader.ts +1 -7
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +14 -9
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/home.ts +1 -13
- package/src/daemon/message-types/messages.ts +66 -7
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/sync.ts +14 -0
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- 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/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/events/relationship-state-updated.ts +25 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -2
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +85 -0
- package/src/home/suggested-prompts.ts +168 -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__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +327 -6
- package/src/memory/auto-analysis-enqueue.ts +5 -1
- package/src/memory/conversation-crud.ts +191 -100
- 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 +26 -0
- package/src/memory/db-maintenance.ts +30 -21
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- 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 +68 -15
- 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-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +11 -3
- package/src/memory/memory-retrospective-job.ts +413 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +41 -14
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- 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 +34 -0
- package/src/memory/migrations/registry.ts +58 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +22 -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-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +158 -112
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
- package/src/memory/v2/__tests__/router.test.ts +660 -4
- package/src/memory/v2/consolidation-job.ts +14 -0
- 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-events.ts +101 -0
- package/src/memory/v2/injection.ts +42 -25
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/prompts/router.ts +26 -1
- package/src/memory/v2/qdrant.ts +14 -2
- package/src/memory/v2/router.ts +369 -62
- 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/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/adapters/macos.ts +18 -1
- package/src/notifications/adapters/platform.ts +1 -1
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/decision-engine.ts +1 -4
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +38 -50
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +8 -5
- package/src/permissions/question-prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +6 -3
- 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 +100 -20
- 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__/system-prompt.test.ts +46 -2
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/persona-resolver.ts +36 -21
- package/src/prompts/sections.ts +69 -19
- package/src/prompts/system-prompt.ts +118 -216
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +10 -2
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +281 -9
- 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 +159 -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/fireworks/client.ts +20 -2
- package/src/providers/gemini/client.ts +49 -6
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +18 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/minimax/client.ts +106 -0
- package/src/providers/model-catalog.ts +91 -1
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openai/chat-completions-provider.ts +63 -23
- package/src/providers/openai/codex-models.ts +18 -0
- package/src/providers/openai/responses-provider.ts +86 -23
- package/src/providers/openrouter/client.ts +5 -1
- 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/types.ts +25 -0
- package/src/providers/usage-tracking.ts +2 -0
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +152 -56
- package/src/runtime/assistant-event-hub.ts +76 -6
- package/src/runtime/auth/route-policy.ts +43 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/btw-sidechain.ts +0 -6
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -6
- package/src/runtime/migrations/vbundle-builder.ts +10 -3
- package/src/runtime/pending-interactions.ts +50 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +161 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +290 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/acp-routes.test.ts +255 -6
- package/src/runtime/routes/acp-routes.ts +8 -1
- package/src/runtime/routes/approval-routes.ts +4 -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/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +147 -2
- 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 +196 -31
- package/src/runtime/routes/conversation-routes.ts +472 -425
- package/src/runtime/routes/conversation-starter-routes.ts +6 -3
- package/src/runtime/routes/disk-pressure-routes.ts +1 -1
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- 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/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +17 -2
- package/src/runtime/routes/host-cu-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +20 -4
- 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/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-item-routes.ts +8 -3
- package/src/runtime/routes/memory-v2-routes.ts +427 -0
- package/src/runtime/routes/memory-v3-routes.ts +316 -0
- package/src/runtime/routes/migration-routes.ts +21 -24
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/plugins-routes.ts +337 -0
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/rename-conversation-routes.ts +6 -2
- package/src/runtime/routes/sanity-routes.ts +159 -0
- 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 +188 -0
- package/src/runtime/routes/workspace-routes.ts +25 -10
- package/src/runtime/services/conversation-serializer.ts +30 -4
- 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/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- 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 +150 -0
- package/src/tools/browser/browser-execution.ts +106 -0
- package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +4 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +22 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +42 -2
- package/src/tools/browser/cdp-client/factory.ts +171 -4
- package/src/tools/browser/cdp-client/local-cdp-client.ts +21 -0
- package/src/tools/browser/cdp-client/types.ts +101 -0
- package/src/tools/browser/pinned-tabs.ts +146 -0
- 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-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +187 -2
- 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/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +216 -73
- package/src/tools/network/web-search.ts +216 -98
- 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/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +3 -9
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/tool-defaults.ts +94 -0
- package/src/tools/types.ts +31 -98
- package/src/tools/ui-surface/definitions.ts +9 -23
- package/src/types/onboarding-context.ts +4 -0
- package/src/usage/pricing.ts +23 -0
- package/src/usage/types.ts +12 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/logger.ts +16 -7
- package/src/util/platform.ts +7 -7
- package/src/util/sqlite3-runtime.ts +65 -0
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
- package/src/workspace/migrations/registry.ts +4 -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/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/prompts/cache-boundary.ts +0 -8
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route handlers for conversation messages and suggestions.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
existsSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
statSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import { join, relative } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
5
|
|
|
13
6
|
import { z } from "zod";
|
|
14
7
|
|
|
@@ -27,10 +20,15 @@ import {
|
|
|
27
20
|
} from "../../channels/types.js";
|
|
28
21
|
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
29
22
|
import { getConfig } from "../../config/loader.js";
|
|
23
|
+
import {
|
|
24
|
+
mergeConsecutiveAssistantMessages,
|
|
25
|
+
mergeToolResultsIntoAssistantMessages,
|
|
26
|
+
} from "../../conversations/message-consolidation.js";
|
|
30
27
|
import { createApprovalConversationGenerator } from "../../daemon/approval-generators.js";
|
|
31
28
|
import type { Conversation } from "../../daemon/conversation.js";
|
|
32
29
|
import {
|
|
33
30
|
buildModelInfoEvent,
|
|
31
|
+
formatCleanResult,
|
|
34
32
|
formatCompactResult,
|
|
35
33
|
isModelSlashCommand,
|
|
36
34
|
} from "../../daemon/conversation-process.js";
|
|
@@ -41,6 +39,7 @@ import {
|
|
|
41
39
|
import { getOrCreateConversation as getOrCreateConversationInstance } from "../../daemon/conversation-store.js";
|
|
42
40
|
import { canonicalizeTimeZone } from "../../daemon/date-context.js";
|
|
43
41
|
import {
|
|
42
|
+
buildScanFirstMessage,
|
|
44
43
|
getCannedFirstGreeting,
|
|
45
44
|
isWakeUpGreeting,
|
|
46
45
|
} from "../../daemon/first-greeting.js";
|
|
@@ -51,6 +50,7 @@ import {
|
|
|
51
50
|
preactivateHostProxySkills,
|
|
52
51
|
shouldAttachHostProxyForCapability,
|
|
53
52
|
} from "../../daemon/host-proxy-preactivation.js";
|
|
53
|
+
import { getAssistantName } from "../../daemon/identity-helpers.js";
|
|
54
54
|
import type { ServerMessage } from "../../daemon/message-protocol.js";
|
|
55
55
|
import type {
|
|
56
56
|
HostProxyTransportMetadata,
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
} from "../../memory/canonical-guardian-store.js";
|
|
76
76
|
import {
|
|
77
77
|
addMessage,
|
|
78
|
-
|
|
78
|
+
getConversation,
|
|
79
79
|
getMessages,
|
|
80
80
|
getMessagesPaginated,
|
|
81
81
|
hasMessages,
|
|
@@ -103,10 +103,7 @@ import type { Provider } from "../../providers/types.js";
|
|
|
103
103
|
import { checkIngressForSecrets } from "../../security/secret-ingress.js";
|
|
104
104
|
import { getSubagentManager } from "../../subagent/index.js";
|
|
105
105
|
import { getLogger } from "../../util/logger.js";
|
|
106
|
-
import {
|
|
107
|
-
getInterfacesDir,
|
|
108
|
-
getWorkspacePromptPath,
|
|
109
|
-
} from "../../util/platform.js";
|
|
106
|
+
import { getWorkspacePromptPath } from "../../util/platform.js";
|
|
110
107
|
import { silentlyWithLog } from "../../util/silently.js";
|
|
111
108
|
import { assistantEventHub, broadcastMessage } from "../assistant-event-hub.js";
|
|
112
109
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
@@ -128,7 +125,12 @@ import {
|
|
|
128
125
|
resolveTrustContext,
|
|
129
126
|
withSourceChannel,
|
|
130
127
|
} from "../trust-context-resolver.js";
|
|
131
|
-
import {
|
|
128
|
+
import {
|
|
129
|
+
BadRequestError,
|
|
130
|
+
InternalError,
|
|
131
|
+
NotFoundError,
|
|
132
|
+
RouteError,
|
|
133
|
+
} from "./errors.js";
|
|
132
134
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
133
135
|
import { RouteResponse } from "./types.js";
|
|
134
136
|
|
|
@@ -136,6 +138,7 @@ const log = getLogger("conversation-routes");
|
|
|
136
138
|
|
|
137
139
|
/** Matches the `<no_response/>` sentinel used by channel delivery suppression. */
|
|
138
140
|
const NO_RESPONSE_INLINE_RE = /<no_response\s*\/?>/g;
|
|
141
|
+
const ATTACHMENT_ENTRY_RE = /^attachment:(\d+)$/;
|
|
139
142
|
|
|
140
143
|
const SUGGESTION_CACHE_MAX = 100;
|
|
141
144
|
const VALID_RISK_THRESHOLDS = ["none", "low", "medium", "high"] as const;
|
|
@@ -148,31 +151,95 @@ function isValidRiskThreshold(value: unknown): value is RiskThreshold {
|
|
|
148
151
|
);
|
|
149
152
|
}
|
|
150
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
|
+
|
|
182
|
+
/**
|
|
183
|
+
* True when a message's persisted metadata explicitly flags it as hidden.
|
|
184
|
+
* Used to suppress internal scaffolding messages from UI history while
|
|
185
|
+
* leaving them in the LLM-side context.
|
|
186
|
+
*/
|
|
187
|
+
function isHiddenMessage(metadata: string | null): boolean {
|
|
188
|
+
if (!metadata) return false;
|
|
189
|
+
try {
|
|
190
|
+
const meta = JSON.parse(metadata) as { hidden?: unknown };
|
|
191
|
+
return meta?.hidden === true;
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
function buildSlackHistoryMessage(
|
|
152
198
|
slackMeta: SlackMessageMetadata | null,
|
|
199
|
+
opts?: { role?: string; assistantDisplayName?: string },
|
|
153
200
|
): RuntimeMessagePayload["slackMessage"] | undefined {
|
|
154
201
|
if (!slackMeta) return undefined;
|
|
155
202
|
|
|
156
203
|
const slackConfig = getConfig().slack;
|
|
204
|
+
const replyThreadTs =
|
|
205
|
+
slackMeta.threadTs && slackMeta.threadTs !== slackMeta.channelTs
|
|
206
|
+
? slackMeta.threadTs
|
|
207
|
+
: undefined;
|
|
157
208
|
const messageLink = buildSlackMessageDeepLinks({
|
|
158
209
|
teamId: slackConfig?.teamId,
|
|
159
210
|
teamUrl: slackConfig?.teamUrl,
|
|
160
211
|
channelId: slackMeta.channelId,
|
|
161
212
|
messageTs: slackMeta.channelTs,
|
|
213
|
+
...(replyThreadTs ? { threadTs: replyThreadTs } : {}),
|
|
162
214
|
});
|
|
163
|
-
const threadLink =
|
|
215
|
+
const threadLink = replyThreadTs
|
|
164
216
|
? buildSlackMessageDeepLinks({
|
|
165
217
|
teamId: slackConfig?.teamId,
|
|
166
218
|
teamUrl: slackConfig?.teamUrl,
|
|
167
219
|
channelId: slackMeta.channelId,
|
|
168
|
-
messageTs:
|
|
220
|
+
messageTs: replyThreadTs,
|
|
169
221
|
})
|
|
170
222
|
: undefined;
|
|
223
|
+
const assistantDisplayName =
|
|
224
|
+
opts?.role === "assistant" ? opts.assistantDisplayName : undefined;
|
|
225
|
+
const senderDisplayName =
|
|
226
|
+
slackMeta.displayName?.trim() || assistantDisplayName;
|
|
171
227
|
|
|
172
228
|
return {
|
|
173
229
|
channelId: slackMeta.channelId,
|
|
230
|
+
...(slackMeta.channelName ? { channelName: slackMeta.channelName } : {}),
|
|
174
231
|
channelTs: slackMeta.channelTs,
|
|
175
232
|
...(slackMeta.threadTs ? { threadTs: slackMeta.threadTs } : {}),
|
|
233
|
+
...(senderDisplayName || slackMeta.actorExternalUserId
|
|
234
|
+
? {
|
|
235
|
+
sender: {
|
|
236
|
+
...(senderDisplayName ? { displayName: senderDisplayName } : {}),
|
|
237
|
+
...(slackMeta.actorExternalUserId
|
|
238
|
+
? { externalUserId: slackMeta.actorExternalUserId }
|
|
239
|
+
: {}),
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
176
243
|
...(messageLink ? { messageLink } : {}),
|
|
177
244
|
...(threadLink ? { threadLink } : {}),
|
|
178
245
|
};
|
|
@@ -253,6 +320,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
253
320
|
verifiedActorExternalUserId?: string;
|
|
254
321
|
/** Verified actor principal ID for principal-based authorization. */
|
|
255
322
|
verifiedActorPrincipalId?: string;
|
|
323
|
+
/** Originating client identifier for sync_changed self-echo suppression. */
|
|
324
|
+
originClientId?: string;
|
|
256
325
|
}): Promise<{ consumed: boolean; messageId?: string }> {
|
|
257
326
|
const {
|
|
258
327
|
conversationId,
|
|
@@ -265,6 +334,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
265
334
|
approvalConversationGenerator,
|
|
266
335
|
verifiedActorExternalUserId,
|
|
267
336
|
verifiedActorPrincipalId,
|
|
337
|
+
originClientId,
|
|
268
338
|
} = params;
|
|
269
339
|
const trimmedContent = content.trim();
|
|
270
340
|
|
|
@@ -362,7 +432,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
362
432
|
? "Decision applied."
|
|
363
433
|
: "Request already resolved.");
|
|
364
434
|
const assistantMessage = createAssistantMessage(replyText);
|
|
365
|
-
await addMessage(
|
|
435
|
+
const persistedAssistant = await addMessage(
|
|
366
436
|
conversationId,
|
|
367
437
|
"assistant",
|
|
368
438
|
JSON.stringify(assistantMessage.content),
|
|
@@ -377,9 +447,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
377
447
|
text: replyText,
|
|
378
448
|
conversationId: conversationId,
|
|
379
449
|
});
|
|
380
|
-
onEvent
|
|
450
|
+
emitCannedMessageComplete(onEvent, conversationId, persistedAssistant.id);
|
|
381
451
|
}
|
|
382
|
-
publishConversationMessagesChanged(conversationId);
|
|
452
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
383
453
|
} catch (err) {
|
|
384
454
|
log.warn(
|
|
385
455
|
{ err, conversationId },
|
|
@@ -390,32 +460,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
390
460
|
return { consumed: true, messageId };
|
|
391
461
|
}
|
|
392
462
|
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
):
|
|
396
|
-
if (!interfacesDir || !existsSync(interfacesDir)) return [];
|
|
397
|
-
const results: Array<{ path: string; mtimeMs: number }> = [];
|
|
398
|
-
const scan = (dir: string): void => {
|
|
399
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
400
|
-
const fullPath = join(dir, entry.name);
|
|
401
|
-
if (entry.isDirectory()) {
|
|
402
|
-
scan(fullPath);
|
|
403
|
-
} else {
|
|
404
|
-
results.push({
|
|
405
|
-
path: relative(interfacesDir, fullPath),
|
|
406
|
-
mtimeMs: statSync(fullPath).mtimeMs,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
scan(interfacesDir);
|
|
412
|
-
return results;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export function handleListMessages(
|
|
416
|
-
{ queryParams }: RouteHandlerArgs,
|
|
417
|
-
interfacesDir: string | null,
|
|
418
|
-
): Record<string, unknown> {
|
|
463
|
+
export function handleListMessages({
|
|
464
|
+
queryParams,
|
|
465
|
+
}: RouteHandlerArgs): Record<string, unknown> {
|
|
419
466
|
const conversationId = queryParams?.conversationId;
|
|
420
467
|
const conversationKey = queryParams?.conversationKey;
|
|
421
468
|
|
|
@@ -423,8 +470,20 @@ export function handleListMessages(
|
|
|
423
470
|
if (conversationId) {
|
|
424
471
|
resolvedConversationId = conversationId;
|
|
425
472
|
} else if (conversationKey) {
|
|
473
|
+
// Dual lookup, key-first: prefer the `conversation_keys` table — the
|
|
474
|
+
// canonical channel/external → internal-id mapping — so legacy or
|
|
475
|
+
// externally-sourced keys keep their explicit mapping precedence and
|
|
476
|
+
// never collide with an unrelated `conversations.id`. Fall back to a
|
|
477
|
+
// direct id lookup only when no mapping exists, which covers
|
|
478
|
+
// background/scheduled conversations bootstrapped without a
|
|
479
|
+
// `conversation_keys` row (web clients use the conversation list's
|
|
480
|
+
// `id` as `conversationKey` for those).
|
|
426
481
|
const mapping = getConversationByKey(conversationKey);
|
|
427
|
-
|
|
482
|
+
if (mapping) {
|
|
483
|
+
resolvedConversationId = mapping.conversationId;
|
|
484
|
+
} else if (getConversation(conversationKey)) {
|
|
485
|
+
resolvedConversationId = conversationKey;
|
|
486
|
+
}
|
|
428
487
|
} else {
|
|
429
488
|
throw new BadRequestError(
|
|
430
489
|
"conversationKey or conversationId query parameter is required",
|
|
@@ -480,16 +539,30 @@ export function handleListMessages(
|
|
|
480
539
|
let rawMessages: MessageRow[];
|
|
481
540
|
let hasMore = false;
|
|
482
541
|
|
|
542
|
+
// Drop messages flagged as hidden in metadata (e.g. internal scaffolding
|
|
543
|
+
// like retrospective instructions). The LLM-side history loader
|
|
544
|
+
// (`getMessages` in memory/conversation-crud.ts) intentionally does not
|
|
545
|
+
// filter — hidden messages remain in agent context but are suppressed from
|
|
546
|
+
// the UI list. Filtering is pushed into the paginated query so `hasMore`
|
|
547
|
+
// and the cursor reflect visible rows; otherwise a fully-hidden page would
|
|
548
|
+
// return `hasMore: true` with no cursor and stall the web client.
|
|
549
|
+
// Hidden tool_use/tool_result pairs must be hidden together — if a hidden
|
|
550
|
+
// assistant message has tool_use blocks but its matching user tool_result
|
|
551
|
+
// is left visible, the result will render as a standalone orphan because
|
|
552
|
+
// `mergeToolResultsIntoAssistantMessages` has nothing to merge it into.
|
|
553
|
+
const visibleFilter = (m: MessageRow) => !isHiddenMessage(m.metadata);
|
|
554
|
+
|
|
483
555
|
if (isPaginated) {
|
|
484
556
|
const result = getMessagesPaginated(
|
|
485
557
|
resolvedConversationId,
|
|
486
558
|
limit,
|
|
487
559
|
beforeTimestamp,
|
|
560
|
+
visibleFilter,
|
|
488
561
|
);
|
|
489
562
|
rawMessages = result.messages;
|
|
490
563
|
hasMore = result.hasMore;
|
|
491
564
|
} else {
|
|
492
|
-
rawMessages = getMessages(resolvedConversationId);
|
|
565
|
+
rawMessages = getMessages(resolvedConversationId).filter(visibleFilter);
|
|
493
566
|
}
|
|
494
567
|
|
|
495
568
|
// During streaming, tool_use (assistant) and tool_result (user) events are
|
|
@@ -508,6 +581,7 @@ export function handleListMessages(
|
|
|
508
581
|
// (consecutive tool refs grouped together).
|
|
509
582
|
const { messages: consolidatedMessages, mergedIdMap } =
|
|
510
583
|
mergeConsecutiveAssistantMessages(mergedMessages);
|
|
584
|
+
const assistantSlackDisplayName = getAssistantName()?.trim() || undefined;
|
|
511
585
|
|
|
512
586
|
// Parse content blocks and extract text + tool calls
|
|
513
587
|
const parsed = consolidatedMessages.map((msg) => {
|
|
@@ -559,6 +633,10 @@ export function handleListMessages(
|
|
|
559
633
|
}
|
|
560
634
|
const slackMessage = buildSlackHistoryMessage(
|
|
561
635
|
readSlackMetadataFromMessageMetadata(msg.metadata),
|
|
636
|
+
{
|
|
637
|
+
role: msg.role,
|
|
638
|
+
assistantDisplayName: assistantSlackDisplayName,
|
|
639
|
+
},
|
|
562
640
|
);
|
|
563
641
|
|
|
564
642
|
// Strip <no_response/> markers from assistant messages so web/API
|
|
@@ -599,6 +677,7 @@ export function handleListMessages(
|
|
|
599
677
|
textSegments: filteredSegments,
|
|
600
678
|
contentOrder: filteredContentOrder,
|
|
601
679
|
surfaces: rendered.surfaces,
|
|
680
|
+
attachmentRefs: rendered.attachments,
|
|
602
681
|
slackMessage,
|
|
603
682
|
...(rendered.thinkingSegments.length > 0
|
|
604
683
|
? { thinkingSegments: rendered.thinkingSegments }
|
|
@@ -618,6 +697,7 @@ export function handleListMessages(
|
|
|
618
697
|
textSegments: rendered.textSegments,
|
|
619
698
|
contentOrder: rendered.contentOrder,
|
|
620
699
|
surfaces: rendered.surfaces,
|
|
700
|
+
attachmentRefs: rendered.attachments,
|
|
621
701
|
slackMessage,
|
|
622
702
|
...(rendered.thinkingSegments.length > 0
|
|
623
703
|
? { thinkingSegments: rendered.thinkingSegments }
|
|
@@ -627,15 +707,6 @@ export function handleListMessages(
|
|
|
627
707
|
};
|
|
628
708
|
});
|
|
629
709
|
|
|
630
|
-
const interfaceFiles = getInterfaceFilesWithMtimes(interfacesDir);
|
|
631
|
-
|
|
632
|
-
let prevAssistantTimestamp = 0;
|
|
633
|
-
if (isPaginated && rawMessages.length > 0) {
|
|
634
|
-
prevAssistantTimestamp = getLastAssistantTimestampBefore(
|
|
635
|
-
resolvedConversationId!,
|
|
636
|
-
rawMessages[0].createdAt,
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
710
|
const messages: RuntimeMessagePayload[] = parsed.map((m) => {
|
|
640
711
|
let msgAttachments: RuntimeAttachmentMetadata[] = [];
|
|
641
712
|
if (m.id) {
|
|
@@ -682,19 +753,76 @@ export function handleListMessages(
|
|
|
682
753
|
}
|
|
683
754
|
}
|
|
684
755
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
756
|
+
// Align msgAttachments order with the file-block order captured by
|
|
757
|
+
// renderHistoryContent. When a file block was persisted with
|
|
758
|
+
// `_attachmentId`, we can join on that id to position the chip inline
|
|
759
|
+
// (the `attachment:N` entries in contentOrder index into msgAttachments).
|
|
760
|
+
// DB rows without a matching ref go to the tail as orphan chips;
|
|
761
|
+
// unmatched refs drop their contentOrder entry and trigger a remap.
|
|
762
|
+
let alignedContentOrder = m.contentOrder;
|
|
763
|
+
if (
|
|
764
|
+
m.attachmentRefs.length > 0 &&
|
|
765
|
+
msgAttachments.length > 0 &&
|
|
766
|
+
m.contentOrder.length > 0
|
|
767
|
+
) {
|
|
768
|
+
const byId = new Map<string, number>();
|
|
769
|
+
msgAttachments.forEach((att, idx) => {
|
|
770
|
+
if (att.id) byId.set(att.id, idx);
|
|
771
|
+
});
|
|
772
|
+
const consumed = new Set<number>();
|
|
773
|
+
const orderedRowIdx: Array<number | null> = m.attachmentRefs.map(
|
|
774
|
+
(ref) => {
|
|
775
|
+
if (!ref.attachmentId) return null;
|
|
776
|
+
const idx = byId.get(ref.attachmentId);
|
|
777
|
+
if (idx === undefined || consumed.has(idx)) return null;
|
|
778
|
+
consumed.add(idx);
|
|
779
|
+
return idx;
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
const matchedRows = orderedRowIdx.filter(
|
|
783
|
+
(idx): idx is number => idx !== null,
|
|
784
|
+
);
|
|
785
|
+
if (matchedRows.length > 0) {
|
|
786
|
+
const orphanRows: number[] = [];
|
|
787
|
+
for (let i = 0; i < msgAttachments.length; i++) {
|
|
788
|
+
if (!consumed.has(i)) orphanRows.push(i);
|
|
789
|
+
}
|
|
790
|
+
msgAttachments = [
|
|
791
|
+
...matchedRows.map((i) => msgAttachments[i]),
|
|
792
|
+
...orphanRows.map((i) => msgAttachments[i]),
|
|
793
|
+
];
|
|
794
|
+
const refToNewIdx = new Map<number, number>();
|
|
795
|
+
let nextIdx = 0;
|
|
796
|
+
orderedRowIdx.forEach((rowIdx, refIdx) => {
|
|
797
|
+
if (rowIdx !== null) {
|
|
798
|
+
refToNewIdx.set(refIdx, nextIdx);
|
|
799
|
+
nextIdx++;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
alignedContentOrder = m.contentOrder
|
|
803
|
+
.map((entry) => {
|
|
804
|
+
const match = entry.match(ATTACHMENT_ENTRY_RE);
|
|
805
|
+
if (!match) return entry;
|
|
806
|
+
const remapped = refToNewIdx.get(Number(match[1]));
|
|
807
|
+
return remapped !== undefined
|
|
808
|
+
? `attachment:${remapped}`
|
|
809
|
+
: undefined;
|
|
810
|
+
})
|
|
811
|
+
.filter((e): e is string => e !== undefined);
|
|
812
|
+
} else {
|
|
813
|
+
// No refs carried an attachmentId we could match — strip any
|
|
814
|
+
// attachment:N entries so the client doesn't try to position
|
|
815
|
+
// attachments inline against a misaligned array.
|
|
816
|
+
alignedContentOrder = m.contentOrder.filter(
|
|
817
|
+
(entry) => !ATTACHMENT_ENTRY_RE.test(entry),
|
|
818
|
+
);
|
|
696
819
|
}
|
|
697
|
-
|
|
820
|
+
} else if (m.attachmentRefs.length > 0 && msgAttachments.length === 0) {
|
|
821
|
+
// Refs were captured but no DB rows came back — drop the
|
|
822
|
+
// contentOrder entries to avoid out-of-bounds renders.
|
|
823
|
+
alignedContentOrder = m.contentOrder.filter(
|
|
824
|
+
(entry) => !ATTACHMENT_ENTRY_RE.test(entry),
|
|
825
|
+
);
|
|
698
826
|
}
|
|
699
827
|
|
|
700
828
|
// Use sentAt (actual event time) for the display timestamp when
|
|
@@ -704,26 +832,21 @@ export function handleListMessages(
|
|
|
704
832
|
// on createdAt. The mismatch is benign — it may return slightly extra
|
|
705
833
|
// data on a page boundary but never loses messages.
|
|
706
834
|
const displayTimestamp = m.sentAt ?? m.timestamp;
|
|
707
|
-
const mergedMessageIds = mergedIdMap.get(m.id) ?? [];
|
|
708
|
-
const daemonMessageId =
|
|
709
|
-
m.role === "assistant"
|
|
710
|
-
? (mergedMessageIds[mergedMessageIds.length - 1] ?? m.id)
|
|
711
|
-
: undefined;
|
|
712
835
|
return {
|
|
713
836
|
id: m.id ?? "",
|
|
714
|
-
...(daemonMessageId ? { daemonMessageId } : {}),
|
|
715
837
|
role: m.role,
|
|
716
838
|
content: m.text,
|
|
717
839
|
timestamp: new Date(displayTimestamp).toISOString(),
|
|
718
840
|
attachments: msgAttachments,
|
|
719
841
|
...(m.toolCalls.length > 0 ? { toolCalls: m.toolCalls } : {}),
|
|
720
|
-
...(interfaces ? { interfaces } : {}),
|
|
721
842
|
...(m.surfaces.length > 0 ? { surfaces: m.surfaces } : {}),
|
|
722
843
|
...(m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}),
|
|
723
844
|
...(m.thinkingSegments?.length
|
|
724
845
|
? { thinkingSegments: m.thinkingSegments }
|
|
725
846
|
: {}),
|
|
726
|
-
...(
|
|
847
|
+
...(alignedContentOrder.length > 0
|
|
848
|
+
? { contentOrder: alignedContentOrder }
|
|
849
|
+
: {}),
|
|
727
850
|
...(m.subagentNotification
|
|
728
851
|
? { subagentNotification: m.subagentNotification }
|
|
729
852
|
: {}),
|
|
@@ -760,305 +883,6 @@ export function handleListMessages(
|
|
|
760
883
|
return { messages };
|
|
761
884
|
}
|
|
762
885
|
|
|
763
|
-
// ── Tool-result merging ─────────────────────────────────────────────
|
|
764
|
-
|
|
765
|
-
function isToolResultType(type: string): boolean {
|
|
766
|
-
return type === "tool_result" || type === "web_search_tool_result";
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function isSystemNoticeText(block: Record<string, unknown>): boolean {
|
|
770
|
-
if (block.type !== "text") return false;
|
|
771
|
-
const text = typeof block.text === "string" ? block.text : "";
|
|
772
|
-
return (
|
|
773
|
-
text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
|
|
774
|
-
);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Merge tool_result blocks from user messages into the preceding assistant
|
|
779
|
-
* message's content array. This lets renderHistoryContent's pendingToolUses
|
|
780
|
-
* map pair tool_use and tool_result blocks, preventing "unknown" tool names.
|
|
781
|
-
*
|
|
782
|
-
* User messages that consist entirely of tool_result blocks (and optional
|
|
783
|
-
* system_notice text) are removed from the output. Mixed messages (tool_result
|
|
784
|
-
* + real user text) keep only the non-tool-result blocks.
|
|
785
|
-
*/
|
|
786
|
-
function mergeToolResultsIntoAssistantMessages(
|
|
787
|
-
messages: MessageRow[],
|
|
788
|
-
): MessageRow[] {
|
|
789
|
-
// Index of the most recent assistant message in the output array.
|
|
790
|
-
let lastAssistantIdx = -1;
|
|
791
|
-
// Parsed content caches — lazily populated per assistant message.
|
|
792
|
-
const parsedAssistantContent = new Map<number, unknown[]>();
|
|
793
|
-
|
|
794
|
-
const result: MessageRow[] = [];
|
|
795
|
-
|
|
796
|
-
for (const msg of messages) {
|
|
797
|
-
if (msg.role === "assistant") {
|
|
798
|
-
lastAssistantIdx = result.length;
|
|
799
|
-
result.push(msg);
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// Only process user messages — other roles pass through.
|
|
804
|
-
if (msg.role !== "user") {
|
|
805
|
-
result.push(msg);
|
|
806
|
-
continue;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
let blocks: unknown[];
|
|
810
|
-
try {
|
|
811
|
-
const parsed = JSON.parse(msg.content);
|
|
812
|
-
if (!Array.isArray(parsed)) {
|
|
813
|
-
result.push(msg);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
blocks = parsed;
|
|
817
|
-
} catch {
|
|
818
|
-
result.push(msg);
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Separate tool-result blocks from real user content.
|
|
823
|
-
const toolResultBlocks: unknown[] = [];
|
|
824
|
-
const otherBlocks: unknown[] = [];
|
|
825
|
-
for (const block of blocks) {
|
|
826
|
-
if (
|
|
827
|
-
typeof block === "object" &&
|
|
828
|
-
block !== null &&
|
|
829
|
-
typeof (block as Record<string, unknown>).type === "string"
|
|
830
|
-
) {
|
|
831
|
-
const rec = block as Record<string, unknown>;
|
|
832
|
-
if (isToolResultType(rec.type as string)) {
|
|
833
|
-
toolResultBlocks.push(block);
|
|
834
|
-
} else if (isSystemNoticeText(rec)) {
|
|
835
|
-
// System notices don't count as user content — drop them when
|
|
836
|
-
// the message is otherwise tool-result-only.
|
|
837
|
-
otherBlocks.push(block);
|
|
838
|
-
} else {
|
|
839
|
-
otherBlocks.push(block);
|
|
840
|
-
}
|
|
841
|
-
} else {
|
|
842
|
-
otherBlocks.push(block);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// No tool results → pass through unchanged. System notices are only
|
|
847
|
-
// injected alongside tool results in the agent loop, so a pure user
|
|
848
|
-
// message (no tool_result blocks) should never be filtered — even if
|
|
849
|
-
// the user's text happens to look like a system_notice tag.
|
|
850
|
-
if (toolResultBlocks.length === 0) {
|
|
851
|
-
result.push(msg);
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Append tool_result blocks to the preceding assistant message's content.
|
|
856
|
-
if (lastAssistantIdx >= 0) {
|
|
857
|
-
const assistant = result[lastAssistantIdx];
|
|
858
|
-
let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
|
|
859
|
-
if (!assistantContent) {
|
|
860
|
-
try {
|
|
861
|
-
const parsed = JSON.parse(assistant.content);
|
|
862
|
-
assistantContent = Array.isArray(parsed) ? parsed : [parsed];
|
|
863
|
-
} catch {
|
|
864
|
-
assistantContent = [];
|
|
865
|
-
}
|
|
866
|
-
parsedAssistantContent.set(lastAssistantIdx, assistantContent);
|
|
867
|
-
}
|
|
868
|
-
assistantContent.push(...toolResultBlocks);
|
|
869
|
-
} else {
|
|
870
|
-
// No preceding assistant message (pagination boundary) — keep the
|
|
871
|
-
// original message as-is to avoid permanent data loss. The preceding
|
|
872
|
-
// assistant tool_use lives in the previous page; dropping the result
|
|
873
|
-
// here would be unrecoverable.
|
|
874
|
-
// Still strip system notices so internal prompt text isn't exposed.
|
|
875
|
-
const filteredBlocks = blocks.filter(
|
|
876
|
-
(b) =>
|
|
877
|
-
!(
|
|
878
|
-
typeof b === "object" &&
|
|
879
|
-
b !== null &&
|
|
880
|
-
isSystemNoticeText(b as Record<string, unknown>)
|
|
881
|
-
),
|
|
882
|
-
);
|
|
883
|
-
result.push({
|
|
884
|
-
...msg,
|
|
885
|
-
content:
|
|
886
|
-
filteredBlocks.length === blocks.length
|
|
887
|
-
? msg.content
|
|
888
|
-
: JSON.stringify(filteredBlocks),
|
|
889
|
-
});
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// If the user message had only tool_result (+ system_notice) blocks,
|
|
894
|
-
// suppress it entirely. Otherwise keep the non-tool-result content.
|
|
895
|
-
const realUserContent = otherBlocks.filter(
|
|
896
|
-
(b) =>
|
|
897
|
-
!(
|
|
898
|
-
typeof b === "object" &&
|
|
899
|
-
b !== null &&
|
|
900
|
-
isSystemNoticeText(b as Record<string, unknown>)
|
|
901
|
-
),
|
|
902
|
-
);
|
|
903
|
-
if (realUserContent.length > 0) {
|
|
904
|
-
result.push({ ...msg, content: JSON.stringify(otherBlocks) });
|
|
905
|
-
}
|
|
906
|
-
// else: tool-result-only → suppressed (results already merged above)
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Write back any modified assistant message content.
|
|
910
|
-
for (const [idx, content] of parsedAssistantContent) {
|
|
911
|
-
result[idx] = { ...result[idx], content: JSON.stringify(content) };
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return result;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// ── Consecutive assistant message merging ────────────────────────────
|
|
918
|
-
|
|
919
|
-
/** Parse a message's JSON content into an array of content blocks. */
|
|
920
|
-
function parseContentBlocks(content: string): unknown[] {
|
|
921
|
-
try {
|
|
922
|
-
const parsed = JSON.parse(content);
|
|
923
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
924
|
-
} catch (err) {
|
|
925
|
-
log.warn(
|
|
926
|
-
{ err },
|
|
927
|
-
"Failed to parse content blocks during assistant message merge",
|
|
928
|
-
);
|
|
929
|
-
return [];
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Append content blocks from a donor message onto a target block array.
|
|
935
|
-
* Parses the donor's JSON content and pushes each block into `target`.
|
|
936
|
-
*/
|
|
937
|
-
function appendContentBlocks(target: unknown[], donorContent: string): void {
|
|
938
|
-
try {
|
|
939
|
-
const parsed = JSON.parse(donorContent);
|
|
940
|
-
if (Array.isArray(parsed)) {
|
|
941
|
-
target.push(...parsed);
|
|
942
|
-
} else {
|
|
943
|
-
target.push(parsed);
|
|
944
|
-
}
|
|
945
|
-
} catch (err) {
|
|
946
|
-
log.warn(
|
|
947
|
-
{ err },
|
|
948
|
-
"Failed to parse donor content blocks during assistant message merge",
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
/**
|
|
954
|
-
* Promote metadata fields from a donor message to the surviving message
|
|
955
|
-
* when the survivor lacks them. Currently promotes `subagentNotification`.
|
|
956
|
-
* Returns a new MessageRow if promotion occurred, otherwise the original.
|
|
957
|
-
*/
|
|
958
|
-
function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
|
|
959
|
-
if (donor.metadata && survivor.metadata) {
|
|
960
|
-
try {
|
|
961
|
-
const survivorMeta = JSON.parse(survivor.metadata);
|
|
962
|
-
const donorMeta = JSON.parse(donor.metadata);
|
|
963
|
-
if (
|
|
964
|
-
!survivorMeta.subagentNotification &&
|
|
965
|
-
donorMeta.subagentNotification
|
|
966
|
-
) {
|
|
967
|
-
survivorMeta.subagentNotification = donorMeta.subagentNotification;
|
|
968
|
-
return { ...survivor, metadata: JSON.stringify(survivorMeta) };
|
|
969
|
-
}
|
|
970
|
-
} catch (err) {
|
|
971
|
-
log.warn(
|
|
972
|
-
{ err },
|
|
973
|
-
"Failed to parse metadata during assistant message merge",
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
} else if (donor.metadata && !survivor.metadata) {
|
|
977
|
-
return { ...survivor, metadata: donor.metadata };
|
|
978
|
-
}
|
|
979
|
-
return survivor;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* Merge consecutive assistant messages into a single message at query time.
|
|
984
|
-
*
|
|
985
|
-
* During streaming, all assistant turns within one agent loop accumulate on
|
|
986
|
-
* a single client-side ChatMessage. In the DB, each API turn is stored as a
|
|
987
|
-
* separate assistant row (consolidation is deferred to compaction for
|
|
988
|
-
* prefix-cache stability). This produces N separate assistant messages that
|
|
989
|
-
* the client renders as N individual bubbles — each showing "Completed 1
|
|
990
|
-
* step" instead of one grouped "Completed N steps" accordion.
|
|
991
|
-
*
|
|
992
|
-
* This function concatenates the content block arrays of consecutive
|
|
993
|
-
* assistant messages (no intervening user messages after tool-result
|
|
994
|
-
* merging) into the first message of each run. The merged messages are
|
|
995
|
-
* removed from the output. This is query-time only — the DB is not
|
|
996
|
-
* modified.
|
|
997
|
-
*
|
|
998
|
-
* The first message in each run keeps its id, createdAt, and metadata so
|
|
999
|
-
* that attachment lookups, display timestamps, and subagent notifications
|
|
1000
|
-
* continue to work. Metadata from later messages in the run (e.g.
|
|
1001
|
-
* subagentNotification) is preserved by promoting it to the surviving
|
|
1002
|
-
* message when the surviving message has no metadata of its own for that
|
|
1003
|
-
* field.
|
|
1004
|
-
*/
|
|
1005
|
-
function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
|
|
1006
|
-
messages: MessageRow[];
|
|
1007
|
-
/** Maps each surviving message ID → all original message IDs merged into it. */
|
|
1008
|
-
mergedIdMap: Map<string, string[]>;
|
|
1009
|
-
} {
|
|
1010
|
-
const result: MessageRow[] = [];
|
|
1011
|
-
// Key = index in `result`, value = accumulated content blocks.
|
|
1012
|
-
const pendingMerges = new Map<number, unknown[]>();
|
|
1013
|
-
// Key = index in `result`, value = IDs of messages merged into the target.
|
|
1014
|
-
const mergedIds = new Map<number, string[]>();
|
|
1015
|
-
|
|
1016
|
-
for (const msg of messages) {
|
|
1017
|
-
const lastIdx = result.length - 1;
|
|
1018
|
-
const isConsecutiveAssistant =
|
|
1019
|
-
msg.role === "assistant" &&
|
|
1020
|
-
lastIdx >= 0 &&
|
|
1021
|
-
result[lastIdx].role === "assistant";
|
|
1022
|
-
|
|
1023
|
-
if (!isConsecutiveAssistant) {
|
|
1024
|
-
result.push(msg);
|
|
1025
|
-
continue;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// Track the donor message ID.
|
|
1029
|
-
let ids = mergedIds.get(lastIdx);
|
|
1030
|
-
if (!ids) {
|
|
1031
|
-
ids = [];
|
|
1032
|
-
mergedIds.set(lastIdx, ids);
|
|
1033
|
-
}
|
|
1034
|
-
ids.push(msg.id);
|
|
1035
|
-
|
|
1036
|
-
// Lazily parse the target's content on first merge.
|
|
1037
|
-
let targetContent = pendingMerges.get(lastIdx);
|
|
1038
|
-
if (!targetContent) {
|
|
1039
|
-
targetContent = parseContentBlocks(result[lastIdx].content);
|
|
1040
|
-
pendingMerges.set(lastIdx, targetContent);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
appendContentBlocks(targetContent, msg.content);
|
|
1044
|
-
result[lastIdx] = promoteMetadata(result[lastIdx], msg);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Write back merged content for any messages that were targets.
|
|
1048
|
-
for (const [idx, content] of pendingMerges) {
|
|
1049
|
-
result[idx] = { ...result[idx], content: JSON.stringify(content) };
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Build the merged ID map keyed by surviving message ID.
|
|
1053
|
-
const mergedIdMap = new Map<string, string[]>();
|
|
1054
|
-
for (const [idx, ids] of mergedIds) {
|
|
1055
|
-
mergedIdMap.set(result[idx].id, ids);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
return { messages: result, mergedIdMap };
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
/**
|
|
1062
886
|
/**
|
|
1063
887
|
* Persist the pre-chat onboarding payload to disk.
|
|
1064
888
|
*
|
|
@@ -1092,6 +916,10 @@ export function persistOnboardingArtifacts(onboarding: {
|
|
|
1092
916
|
tone: string;
|
|
1093
917
|
userName?: string;
|
|
1094
918
|
assistantName?: string;
|
|
919
|
+
priorAssistants?: string[];
|
|
920
|
+
cohort?: string;
|
|
921
|
+
websiteUrl?: string;
|
|
922
|
+
contentSourceUrl?: string;
|
|
1095
923
|
}): void {
|
|
1096
924
|
writeOnboardingSidecar(onboarding);
|
|
1097
925
|
|
|
@@ -1147,6 +975,7 @@ export async function handleSendMessage(
|
|
|
1147
975
|
): Promise<unknown> {
|
|
1148
976
|
const body = (rawBody ?? {}) as {
|
|
1149
977
|
conversationKey?: string;
|
|
978
|
+
conversationId?: string;
|
|
1150
979
|
content?: string;
|
|
1151
980
|
attachmentIds?: string[];
|
|
1152
981
|
sourceChannel?: string;
|
|
@@ -1169,13 +998,23 @@ export async function handleSendMessage(
|
|
|
1169
998
|
assistantName?: string;
|
|
1170
999
|
googleConnected?: boolean;
|
|
1171
1000
|
googleScopes?: string[];
|
|
1001
|
+
priorAssistants?: string[];
|
|
1002
|
+
cohort?: string;
|
|
1003
|
+
websiteUrl?: string;
|
|
1004
|
+
contentSourceUrl?: string;
|
|
1172
1005
|
};
|
|
1173
1006
|
};
|
|
1174
1007
|
|
|
1175
1008
|
const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
|
|
1176
1009
|
const principalType = headers?.["x-vellum-principal-type"];
|
|
1010
|
+
const originClientId =
|
|
1011
|
+
headers?.["x-vellum-client-id"]?.trim() || undefined;
|
|
1177
1012
|
|
|
1178
1013
|
const { conversationKey, content, attachmentIds } = body;
|
|
1014
|
+
const inboundConversationId =
|
|
1015
|
+
typeof body.conversationId === "string" && body.conversationId.length > 0
|
|
1016
|
+
? body.conversationId
|
|
1017
|
+
: undefined;
|
|
1179
1018
|
const clientMessageId =
|
|
1180
1019
|
typeof body.clientMessageId === "string" ? body.clientMessageId : undefined;
|
|
1181
1020
|
const requestedInferenceProfile =
|
|
@@ -1243,12 +1082,6 @@ export async function handleSendMessage(
|
|
|
1243
1082
|
? (canonicalizeTimeZone(body.clientTimezone) ?? undefined)
|
|
1244
1083
|
: undefined;
|
|
1245
1084
|
|
|
1246
|
-
// When conversationKey is omitted, derive a stable default from
|
|
1247
|
-
// sourceChannel + sourceInterface so that repeated calls from the same
|
|
1248
|
-
// channel/interface pair share a single conversation thread.
|
|
1249
|
-
const resolvedConversationKey =
|
|
1250
|
-
conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
|
|
1251
|
-
|
|
1252
1085
|
// Reject non-string content values (numbers, objects, etc.)
|
|
1253
1086
|
if (content != null && typeof content !== "string") {
|
|
1254
1087
|
throw new BadRequestError("content must be a string");
|
|
@@ -1312,9 +1145,40 @@ export async function handleSendMessage(
|
|
|
1312
1145
|
// timer so the next heartbeat is a full interval after this interaction.
|
|
1313
1146
|
HeartbeatService.getInstance()?.resetTimer();
|
|
1314
1147
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
+
}
|
|
1318
1182
|
|
|
1319
1183
|
if (requestedRiskThreshold !== undefined) {
|
|
1320
1184
|
const result = await ipcCall("set_conversation_threshold", {
|
|
@@ -1348,6 +1212,7 @@ export async function handleSendMessage(
|
|
|
1348
1212
|
publishConversationListAndMetadataChanged(
|
|
1349
1213
|
"created",
|
|
1350
1214
|
mapping.conversationId,
|
|
1215
|
+
originClientId,
|
|
1351
1216
|
);
|
|
1352
1217
|
}
|
|
1353
1218
|
}
|
|
@@ -1500,12 +1365,34 @@ export async function handleSendMessage(
|
|
|
1500
1365
|
);
|
|
1501
1366
|
}
|
|
1502
1367
|
|
|
1503
|
-
// ──
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
|
|
1368
|
+
// ── URL scan path: rewrite first message for scan onboarding ──
|
|
1369
|
+
// When onboarding provides a websiteUrl or contentSourceUrl and the
|
|
1370
|
+
// first message is the macOS wake-up greeting, bypass the canned
|
|
1371
|
+
// greeting and rewrite the user message to a scan instruction so real
|
|
1372
|
+
// LLM inference runs against the URL.
|
|
1373
|
+
const sanitizeUrl = (u?: string) =>
|
|
1374
|
+
u?.trim().replace(/[\r\n\t]/g, "") || undefined;
|
|
1375
|
+
const websiteUrl = sanitizeUrl(body.onboarding?.websiteUrl);
|
|
1376
|
+
const contentSourceUrl = sanitizeUrl(body.onboarding?.contentSourceUrl);
|
|
1377
|
+
const scanUrl = websiteUrl || contentSourceUrl;
|
|
1378
|
+
const isWakeUp = isWakeUpGreeting(
|
|
1379
|
+
trimmedContent,
|
|
1380
|
+
conversation.getMessages().length,
|
|
1381
|
+
);
|
|
1382
|
+
const isScanPath = !!scanUrl && isWakeUp;
|
|
1383
|
+
|
|
1384
|
+
let effectiveContent: string | undefined;
|
|
1385
|
+
if (isScanPath) {
|
|
1386
|
+
const scanVariant = websiteUrl
|
|
1387
|
+
? ("website" as const)
|
|
1388
|
+
: ("content-source" as const);
|
|
1389
|
+
effectiveContent = buildScanFirstMessage(scanUrl, scanVariant);
|
|
1390
|
+
// Fall through to normal inference path below
|
|
1391
|
+
} else if (isWakeUp && body.onboarding?.cohort === "content-automation") {
|
|
1392
|
+
effectiveContent = "I want to write articles that rank better in GEO";
|
|
1393
|
+
// Fall through to normal inference path — the bootstrap template
|
|
1394
|
+
// and geo-writing skill handle this message.
|
|
1395
|
+
} else if (isWakeUp) {
|
|
1509
1396
|
const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
|
|
1510
1397
|
|
|
1511
1398
|
conversation.processing = true;
|
|
@@ -1545,7 +1432,7 @@ export async function handleSendMessage(
|
|
|
1545
1432
|
const conversationId = mapping.conversationId;
|
|
1546
1433
|
|
|
1547
1434
|
const assistantMsg = createAssistantMessage(cannedGreeting);
|
|
1548
|
-
await addMessage(
|
|
1435
|
+
const persistedAssistant = await addMessage(
|
|
1549
1436
|
mapping.conversationId,
|
|
1550
1437
|
"assistant",
|
|
1551
1438
|
JSON.stringify(assistantMsg.content),
|
|
@@ -1569,6 +1456,7 @@ export async function handleSendMessage(
|
|
|
1569
1456
|
tone: body.onboarding!.tone,
|
|
1570
1457
|
googleConnected: body.onboarding!.googleConnected,
|
|
1571
1458
|
googleScopes: body.onboarding!.googleScopes,
|
|
1459
|
+
priorAssistants: body.onboarding!.priorAssistants,
|
|
1572
1460
|
});
|
|
1573
1461
|
} catch (err) {
|
|
1574
1462
|
log.warn({ err }, "Failed to record onboarding telemetry event");
|
|
@@ -1588,8 +1476,12 @@ export async function handleSendMessage(
|
|
|
1588
1476
|
text: cannedGreeting,
|
|
1589
1477
|
conversationId,
|
|
1590
1478
|
});
|
|
1591
|
-
|
|
1592
|
-
|
|
1479
|
+
emitCannedMessageComplete(
|
|
1480
|
+
broadcastMessage,
|
|
1481
|
+
conversationId,
|
|
1482
|
+
persistedAssistant.id,
|
|
1483
|
+
);
|
|
1484
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1593
1485
|
conversation.processing = false;
|
|
1594
1486
|
silentlyWithLog(
|
|
1595
1487
|
conversation.drainQueue(),
|
|
@@ -1623,12 +1515,18 @@ export async function handleSendMessage(
|
|
|
1623
1515
|
tone: body.onboarding!.tone,
|
|
1624
1516
|
googleConnected: body.onboarding!.googleConnected,
|
|
1625
1517
|
googleScopes: body.onboarding!.googleScopes,
|
|
1518
|
+
priorAssistants: body.onboarding!.priorAssistants,
|
|
1626
1519
|
});
|
|
1627
1520
|
} catch (err) {
|
|
1628
1521
|
log.warn({ err }, "Failed to record onboarding telemetry event");
|
|
1629
1522
|
}
|
|
1630
1523
|
}
|
|
1631
1524
|
|
|
1525
|
+
// When the scan path rewrote the first message, prefer the rewritten
|
|
1526
|
+
// content for all downstream consumers (guardian reply, enqueue, agent
|
|
1527
|
+
// loop) so they see the scan instruction rather than the wake-up greeting.
|
|
1528
|
+
const contentAfterScan = effectiveContent ?? content ?? "";
|
|
1529
|
+
|
|
1632
1530
|
const attachments = hasAttachments
|
|
1633
1531
|
? smDeps.resolveAttachments(attachmentIds)
|
|
1634
1532
|
: [];
|
|
@@ -1648,7 +1546,7 @@ export async function handleSendMessage(
|
|
|
1648
1546
|
conversationId: mapping.conversationId,
|
|
1649
1547
|
sourceChannel,
|
|
1650
1548
|
sourceInterface,
|
|
1651
|
-
content:
|
|
1549
|
+
content: contentAfterScan,
|
|
1652
1550
|
attachments,
|
|
1653
1551
|
conversation,
|
|
1654
1552
|
onEvent: broadcastMessage,
|
|
@@ -1661,6 +1559,7 @@ export async function handleSendMessage(
|
|
|
1661
1559
|
: deps.approvalConversationGenerator,
|
|
1662
1560
|
verifiedActorExternalUserId,
|
|
1663
1561
|
verifiedActorPrincipalId,
|
|
1562
|
+
originClientId,
|
|
1664
1563
|
});
|
|
1665
1564
|
if (inlineReplyResult.consumed) {
|
|
1666
1565
|
return {
|
|
@@ -1682,7 +1581,7 @@ export async function handleSendMessage(
|
|
|
1682
1581
|
// Queue the message so it's processed when the current turn completes
|
|
1683
1582
|
const requestId = crypto.randomUUID();
|
|
1684
1583
|
const enqueueResult = conversation.enqueueMessage(
|
|
1685
|
-
|
|
1584
|
+
contentAfterScan,
|
|
1686
1585
|
attachments,
|
|
1687
1586
|
broadcastMessage,
|
|
1688
1587
|
requestId,
|
|
@@ -1754,6 +1653,7 @@ export async function handleSendMessage(
|
|
|
1754
1653
|
accepted: true,
|
|
1755
1654
|
queued: true,
|
|
1756
1655
|
conversationId: mapping.conversationId,
|
|
1656
|
+
requestId,
|
|
1757
1657
|
};
|
|
1758
1658
|
}
|
|
1759
1659
|
|
|
@@ -1801,7 +1701,9 @@ export async function handleSendMessage(
|
|
|
1801
1701
|
await conversation.ensureActorScopedHistory();
|
|
1802
1702
|
|
|
1803
1703
|
// Resolve slash commands before persisting or running the agent loop.
|
|
1804
|
-
|
|
1704
|
+
// `contentAfterScan` already carries the scan-rewritten content when
|
|
1705
|
+
// applicable; reuse it here for consistency.
|
|
1706
|
+
const rawContent = contentAfterScan;
|
|
1805
1707
|
const slashContext = buildSlashContextForContent(rawContent, {
|
|
1806
1708
|
conversationId: mapping.conversationId,
|
|
1807
1709
|
messageCount: conversation.getMessages().length,
|
|
@@ -1846,7 +1748,7 @@ export async function handleSendMessage(
|
|
|
1846
1748
|
conversation.getMessages().push(llmMsg);
|
|
1847
1749
|
|
|
1848
1750
|
const assistantMsg = createAssistantMessage(slashResult.message);
|
|
1849
|
-
await addMessage(
|
|
1751
|
+
const persistedAssistant = await addMessage(
|
|
1850
1752
|
mapping.conversationId,
|
|
1851
1753
|
"assistant",
|
|
1852
1754
|
JSON.stringify(assistantMsg.content),
|
|
@@ -1901,11 +1803,12 @@ export async function handleSendMessage(
|
|
|
1901
1803
|
text: message,
|
|
1902
1804
|
conversationId,
|
|
1903
1805
|
});
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
conversationId
|
|
1907
|
-
|
|
1908
|
-
|
|
1806
|
+
emitCannedMessageComplete(
|
|
1807
|
+
broadcastMessage,
|
|
1808
|
+
conversationId,
|
|
1809
|
+
persistedAssistant.id,
|
|
1810
|
+
);
|
|
1811
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1909
1812
|
conversation.processing = false;
|
|
1910
1813
|
silentlyWithLog(conversation.drainQueue(), "slash-command queue drain");
|
|
1911
1814
|
}, 0);
|
|
@@ -1933,12 +1836,22 @@ export async function handleSendMessage(
|
|
|
1933
1836
|
assistantMessageInterface: sourceInterface,
|
|
1934
1837
|
};
|
|
1935
1838
|
const cleanMsg = createUserMessage(rawContent, attachments);
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
+
}
|
|
1942
1855
|
conversation.getMessages().push(cleanMsg);
|
|
1943
1856
|
|
|
1944
1857
|
const conversationId = mapping.conversationId;
|
|
@@ -1956,7 +1869,7 @@ export async function handleSendMessage(
|
|
|
1956
1869
|
messageId: persisted.id,
|
|
1957
1870
|
clientMessageId,
|
|
1958
1871
|
});
|
|
1959
|
-
publishConversationMessagesChanged(conversationId);
|
|
1872
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1960
1873
|
conversation.emitActivityState(
|
|
1961
1874
|
"thinking",
|
|
1962
1875
|
"context_compacting",
|
|
@@ -1968,7 +1881,7 @@ export async function handleSendMessage(
|
|
|
1968
1881
|
const responseText = formatCompactResult(result);
|
|
1969
1882
|
|
|
1970
1883
|
const assistantMsg = createAssistantMessage(responseText);
|
|
1971
|
-
await addMessage(
|
|
1884
|
+
const persistedAssistant = await addMessage(
|
|
1972
1885
|
conversationId,
|
|
1973
1886
|
"assistant",
|
|
1974
1887
|
JSON.stringify(assistantMsg.content),
|
|
@@ -1982,11 +1895,15 @@ export async function handleSendMessage(
|
|
|
1982
1895
|
text: responseText,
|
|
1983
1896
|
conversationId,
|
|
1984
1897
|
});
|
|
1985
|
-
|
|
1986
|
-
|
|
1898
|
+
emitCannedMessageComplete(
|
|
1899
|
+
broadcastMessage,
|
|
1900
|
+
conversationId,
|
|
1901
|
+
persistedAssistant.id,
|
|
1902
|
+
);
|
|
1903
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1987
1904
|
} catch (err) {
|
|
1988
1905
|
if (assistantMessagePersisted) {
|
|
1989
|
-
publishConversationMessagesChanged(conversationId);
|
|
1906
|
+
publishConversationMessagesChanged(conversationId, originClientId);
|
|
1990
1907
|
}
|
|
1991
1908
|
log.error({ err, conversationId }, "Compact command failed");
|
|
1992
1909
|
broadcastMessage({
|
|
@@ -2012,6 +1929,91 @@ export async function handleSendMessage(
|
|
|
2012
1929
|
};
|
|
2013
1930
|
}
|
|
2014
1931
|
|
|
1932
|
+
if (slashResult.kind === "clean") {
|
|
1933
|
+
conversation.processing = true;
|
|
1934
|
+
const conversationId = mapping.conversationId;
|
|
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.
|
|
1939
|
+
try {
|
|
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),
|
|
1953
|
+
channelMeta,
|
|
1954
|
+
);
|
|
1955
|
+
conversation.getMessages().push(cleanMsg);
|
|
1956
|
+
|
|
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
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
return {
|
|
2007
|
+
accepted: true,
|
|
2008
|
+
messageId: persisted.id,
|
|
2009
|
+
conversationId,
|
|
2010
|
+
};
|
|
2011
|
+
} finally {
|
|
2012
|
+
conversation.processing = false;
|
|
2013
|
+
silentlyWithLog(conversation.drainQueue(), "clean-command queue drain");
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2015
2017
|
const resolvedContent = slashResult.content;
|
|
2016
2018
|
|
|
2017
2019
|
const requestId = crypto.randomUUID();
|
|
@@ -2035,7 +2037,7 @@ export async function handleSendMessage(
|
|
|
2035
2037
|
requestId,
|
|
2036
2038
|
clientMessageId,
|
|
2037
2039
|
});
|
|
2038
|
-
publishConversationMessagesChanged(mapping.conversationId);
|
|
2040
|
+
publishConversationMessagesChanged(mapping.conversationId, originClientId);
|
|
2039
2041
|
|
|
2040
2042
|
// Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
|
|
2041
2043
|
conversation
|
|
@@ -2080,14 +2082,25 @@ async function generateLlmSuggestion(
|
|
|
2080
2082
|
? escapeXmlContent(priorUserText)
|
|
2081
2083
|
: priorUserText;
|
|
2082
2084
|
|
|
2083
|
-
const systemPrompt =
|
|
2084
|
-
"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");
|
|
2085
2097
|
|
|
2086
2098
|
const userPrompt =
|
|
2087
2099
|
`Here is the end of a conversation:\n\n` +
|
|
2088
2100
|
`<user_message>${truncatedUser ?? "(no prior user message)"}</user_message>\n` +
|
|
2089
2101
|
`<assistant_message>${truncatedAssistant}</assistant_message>\n\n` +
|
|
2090
|
-
`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` +
|
|
2091
2104
|
`<reply>YOUR_REPLY_HERE</reply>`;
|
|
2092
2105
|
|
|
2093
2106
|
// Single user message only — no assistant-role prefill. Anthropic
|
|
@@ -2163,14 +2176,27 @@ export async function handleGetSuggestion(
|
|
|
2163
2176
|
};
|
|
2164
2177
|
|
|
2165
2178
|
const conversationKey = queryParams?.conversationKey;
|
|
2166
|
-
|
|
2167
|
-
|
|
2179
|
+
const conversationId = queryParams?.conversationId;
|
|
2180
|
+
if (!conversationKey && !conversationId) {
|
|
2181
|
+
throw new BadRequestError(
|
|
2182
|
+
"conversationKey or conversationId query parameter is required",
|
|
2183
|
+
);
|
|
2168
2184
|
}
|
|
2169
2185
|
|
|
2170
|
-
|
|
2171
|
-
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;
|
|
2172
2198
|
|
|
2173
|
-
const rawMessages = getMessages(
|
|
2199
|
+
const rawMessages = getMessages(resolvedConversationId);
|
|
2174
2200
|
if (rawMessages.length === 0) return noSuggestion;
|
|
2175
2201
|
|
|
2176
2202
|
// Staleness check: compare requested messageId against the latest
|
|
@@ -2369,7 +2395,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2369
2395
|
.optional()
|
|
2370
2396
|
.describe("ID of the oldest message in this page"),
|
|
2371
2397
|
}),
|
|
2372
|
-
handler: (args) => handleListMessages(args
|
|
2398
|
+
handler: (args) => handleListMessages(args),
|
|
2373
2399
|
},
|
|
2374
2400
|
{
|
|
2375
2401
|
operationId: "messages_post",
|
|
@@ -2424,10 +2450,31 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2424
2450
|
description:
|
|
2425
2451
|
"Return an LLM-generated follow-up suggestion for the most recent assistant message.",
|
|
2426
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
|
+
],
|
|
2427
2473
|
responseBody: z.object({
|
|
2428
|
-
suggestion: z.string(),
|
|
2429
|
-
messageId: z.string(),
|
|
2474
|
+
suggestion: z.string().nullable(),
|
|
2475
|
+
messageId: z.string().nullable(),
|
|
2430
2476
|
source: z.string(),
|
|
2477
|
+
stale: z.boolean().optional(),
|
|
2431
2478
|
}),
|
|
2432
2479
|
handler: async (args) =>
|
|
2433
2480
|
handleGetSuggestion(args, {
|