@vellumai/assistant 0.8.5 → 0.8.6
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/AGENTS.md +33 -1
- package/ARCHITECTURE.md +1 -1
- package/bunfig.toml +6 -1
- package/docs/credential-execution-service.md +6 -6
- package/docs/plugins.md +4 -3
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +12 -13
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +4 -1
- package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
- package/openapi.yaml +1900 -166
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +3 -2
- package/src/__tests__/agent-loop-exit-reason.test.ts +102 -9
- package/src/__tests__/agent-loop-override-profile.test.ts +2 -1
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
- package/src/__tests__/annotate-risk-options.test.ts +1 -0
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +9 -13
- package/src/__tests__/assert-not-live-db.ts +79 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +9 -25
- package/src/__tests__/audit-log-rotation.test.ts +2 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
- package/src/__tests__/background-workers-disk-pressure.test.ts +5 -8
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/btw-routes.test.ts +3 -2
- package/src/__tests__/call-controller.test.ts +3 -2
- package/src/__tests__/channel-approval-routes.test.ts +3 -2
- package/src/__tests__/channel-guardian.test.ts +3 -2
- package/src/__tests__/channel-readiness-slack-remote.test.ts +175 -0
- package/src/__tests__/channel-reply-delivery.test.ts +35 -0
- package/src/__tests__/channel-retry-sweep.test.ts +320 -3
- package/src/__tests__/checker.test.ts +12 -12
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/compaction-trail-store.test.ts +264 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
- package/src/__tests__/computer-use-tools.test.ts +12 -14
- package/src/__tests__/config-loader-backfill.test.ts +13 -28
- package/src/__tests__/config-loader-corrupt.test.ts +5 -5
- package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
- package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
- package/src/__tests__/config-schema.test.ts +10 -10
- package/src/__tests__/connection-model-compat.test.ts +83 -0
- package/src/__tests__/contacts-tools.test.ts +3 -2
- package/src/__tests__/context-token-estimator.test.ts +22 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +34 -0
- package/src/__tests__/conversation-agent-loop.test.ts +488 -2
- package/src/__tests__/conversation-analysis-routes.test.ts +1 -0
- package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -0
- package/src/__tests__/conversation-attention-store.test.ts +101 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-error.test.ts +30 -0
- package/src/__tests__/conversation-fork-crud.test.ts +69 -8
- package/src/__tests__/conversation-fork-route.test.ts +3 -2
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
- package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
- package/src/__tests__/conversation-lifecycle.test.ts +1 -0
- package/src/__tests__/conversation-list-source.test.ts +3 -2
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -1
- package/src/__tests__/conversation-load-history-stripped.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +53 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +26 -7
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -0
- package/src/__tests__/conversation-queue.test.ts +333 -291
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -18
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +33 -8
- package/src/__tests__/conversation-routes-slash-commands.test.ts +33 -2
- package/src/__tests__/conversation-runtime-assembly.test.ts +78 -0
- package/src/__tests__/conversation-skill-tools.test.ts +38 -142
- package/src/__tests__/conversation-slash-queue.test.ts +84 -32
- package/src/__tests__/conversation-slash-unknown.test.ts +5 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +46 -0
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
- package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
- package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -3
- package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -17
- package/src/__tests__/conversation-sync-tags.test.ts +128 -12
- package/src/__tests__/conversation-title-service.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +30 -0
- package/src/__tests__/conversation-usage.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
- package/src/__tests__/credential-broker-server-use.test.ts +5 -5
- package/src/__tests__/credential-execution-client.test.ts +72 -1
- package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
- package/src/__tests__/credential-health-service.test.ts +252 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -5
- package/src/__tests__/credential-vault-unit.test.ts +19 -19
- package/src/__tests__/credential-vault.test.ts +5 -5
- package/src/__tests__/cross-provider-web-search.test.ts +56 -2
- package/src/__tests__/db-connection-isolation.test.ts +7 -6
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
- package/src/__tests__/db-test-helpers.ts +58 -0
- package/src/__tests__/disk-pressure-guard.test.ts +58 -41
- package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
- package/src/__tests__/disk-pressure-routes.test.ts +0 -33
- package/src/__tests__/disk-pressure-tools.test.ts +0 -4
- package/src/__tests__/dm-persistence.test.ts +26 -40
- package/src/__tests__/document-create-dedupe.test.ts +189 -0
- package/src/__tests__/document-find-replace.test.ts +3 -2
- package/src/__tests__/document-tool-security.test.ts +81 -2
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
- package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
- package/src/__tests__/encrypted-store.test.ts +11 -9
- package/src/__tests__/feature-flag-test-helpers.ts +53 -0
- package/src/__tests__/filing-service.test.ts +1 -0
- package/src/__tests__/first-greeting.test.ts +62 -12
- package/src/__tests__/gateway-flag-listener.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +26 -0
- package/src/__tests__/guardian-action-sweep.test.ts +3 -2
- package/src/__tests__/guardian-outbound-http.test.ts +3 -2
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/helpers/mock-logger.ts +26 -0
- package/src/__tests__/host-bash-routes.test.ts +1 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
- package/src/__tests__/host-shell-tool.test.ts +5 -4
- package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
- package/src/__tests__/http-conversation-lineage.test.ts +3 -2
- package/src/__tests__/http-user-message-parity.test.ts +29 -7
- package/src/__tests__/identity-intro-cache.test.ts +133 -22
- package/src/__tests__/inbound-slack-persistence.test.ts +44 -72
- package/src/__tests__/inference-profile-reaper.test.ts +3 -2
- package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
- package/src/__tests__/injector-disk-pressure.test.ts +3 -17
- package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
- package/src/__tests__/list-messages-hidden-metadata.test.ts +80 -0
- package/src/__tests__/llm-context-normalization.test.ts +42 -0
- package/src/__tests__/llm-resolver.test.ts +331 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
- package/src/__tests__/mcp-abort-signal.test.ts +14 -0
- package/src/__tests__/mcp-client-auth.test.ts +14 -0
- package/src/__tests__/messaging-send-tool.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +18 -2
- package/src/__tests__/model-intents.test.ts +3 -3
- package/src/__tests__/native-web-search.test.ts +30 -2
- package/src/__tests__/notification-deep-link.test.ts +62 -0
- package/src/__tests__/oauth-commands-routes.test.ts +37 -0
- package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
- package/src/__tests__/oauth-store.test.ts +3 -2
- package/src/__tests__/onboarding-template-contract.test.ts +3 -2
- package/src/__tests__/openai-provider.test.ts +8 -9
- package/src/__tests__/openai-responses-provider.test.ts +70 -10
- package/src/__tests__/openrouter-provider-only.test.ts +27 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +46 -1
- package/src/__tests__/persistence-pipeline.test.ts +139 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +83 -12
- package/src/__tests__/plugin-bootstrap.test.ts +9 -11
- package/src/__tests__/plugin-tool-contribution.test.ts +41 -38
- package/src/__tests__/process-message-background-slack.test.ts +21 -16
- package/src/__tests__/process-message-display-content.test.ts +19 -22
- package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
- package/src/__tests__/provider-platform-proxy-integration.test.ts +216 -4
- package/src/__tests__/provider-registry-ollama.test.ts +45 -22
- package/src/__tests__/recording-handler.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +82 -76
- package/src/__tests__/relay-server.test.ts +10 -10
- package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
- package/src/__tests__/schedule-store.test.ts +16 -1
- package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
- package/src/__tests__/secret-ingress-http.test.ts +5 -1
- package/src/__tests__/secure-keys.test.ts +3 -3
- package/src/__tests__/send-endpoint-busy.test.ts +81 -42
- package/src/__tests__/server-history-render.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +8 -10
- package/src/__tests__/skill-feature-flags.test.ts +14 -16
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -5
- package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
- package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
- package/src/__tests__/skill-tool-factory.test.ts +96 -95
- package/src/__tests__/slack-channel-config.test.ts +3 -3
- package/src/__tests__/subagent-call-site-routing.test.ts +11 -3
- package/src/__tests__/subagent-disposal.test.ts +27 -8
- package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
- package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
- package/src/__tests__/subagent-manager-notify.test.ts +20 -8
- package/src/__tests__/subagent-notify-parent.test.ts +5 -4
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
- package/src/__tests__/subagent-tools.test.ts +2 -1
- package/src/__tests__/suggestion-routes.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +38 -0
- package/src/__tests__/test-preload-verifier.ts +68 -0
- package/src/__tests__/test-preload.ts +32 -39
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
- package/src/__tests__/tool-executor.test.ts +55 -10
- package/src/__tests__/tool-preview-lifecycle.test.ts +1 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/twilio-routes.test.ts +3 -2
- package/src/__tests__/validate-input.test.ts +381 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -1
- package/src/__tests__/voice-session-bridge.test.ts +37 -28
- package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
- package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
- package/src/acp/session-manager.ts +5 -6
- package/src/agent/loop.ts +80 -0
- package/src/api/README.md +124 -2
- package/src/api/constants/call-sites.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +51 -0
- package/src/api/events/assistant-text-delta.ts +32 -0
- package/src/api/events/assistant-turn-start.ts +33 -0
- package/src/api/events/document-comment-created.ts +48 -0
- package/src/api/events/document-comment-deleted.ts +24 -0
- package/src/api/events/document-comment-reopened.ts +25 -0
- package/src/api/events/document-comment-resolved.ts +27 -0
- package/src/api/events/generation-cancelled.ts +24 -0
- package/src/api/events/generation-handoff.ts +41 -0
- package/src/api/events/message-complete.ts +42 -0
- package/src/api/events/open-url.ts +30 -0
- package/src/{events → api/events}/relationship-state-updated.ts +3 -3
- package/src/api/events/tool-use-start.ts +32 -0
- package/src/api/index.ts +128 -3
- package/src/api/responses/llm-context-response.ts +39 -0
- package/src/api/responses/llm-request-log-entry.ts +93 -0
- package/src/api/responses/memory-recall-log.ts +65 -0
- package/src/api/responses/memory-v2-activation-log.ts +78 -0
- package/src/background-wake/background-wake-routes.test.ts +687 -52
- package/src/background-wake/platform-client.test.ts +308 -0
- package/src/background-wake/platform-client.ts +167 -0
- package/src/background-wake/publisher.ts +91 -0
- package/src/background-wake/runtime-registry.ts +2 -2
- package/src/background-wake/wake-intent-hooks.test.ts +282 -0
- package/src/calls/guardian-dispatch.ts +1 -0
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
- package/src/cli/commands/__tests__/notifications.test.ts +184 -40
- package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
- package/src/cli/commands/channels/index.ts +229 -0
- package/src/cli/commands/memory-v3-render.ts +147 -0
- package/src/cli/commands/memory-v3.ts +255 -4
- package/src/cli/commands/notifications.ts +365 -55
- package/src/cli/lib/open-browser.ts +7 -2
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +23 -42
- package/src/config/bundled-skills/document-editor/SKILL.md +5 -1
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
- package/src/config/call-site-defaults.ts +1 -1
- package/src/config/feature-flag-cache.ts +86 -0
- package/src/config/feature-flag-registry.json +17 -17
- package/src/config/llm-context-resolution.ts +10 -1
- package/src/config/llm-resolver.ts +121 -15
- package/src/config/loader.ts +4 -5
- package/src/config/schemas/__tests__/memory-v2.test.ts +15 -0
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +90 -1
- package/src/config/schemas/memory-v2.ts +26 -0
- package/src/config/schemas/services.ts +6 -2
- package/src/config/seed-inference-profiles.ts +36 -16
- package/src/context/token-estimator.ts +10 -5
- package/src/credential-execution/executable-discovery.ts +40 -0
- package/src/credential-execution/process-manager.ts +6 -2
- package/src/credential-health/credential-health-service.ts +125 -40
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +13 -15
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
- package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
- package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +1 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +389 -68
- package/src/daemon/conversation-agent-loop.ts +132 -28
- package/src/daemon/conversation-error.ts +33 -5
- package/src/daemon/conversation-messaging.ts +84 -43
- package/src/daemon/conversation-process.ts +74 -37
- package/src/daemon/conversation-runtime-assembly.ts +29 -9
- package/src/daemon/conversation-skill-tools.ts +14 -30
- package/src/daemon/conversation-surfaces.ts +69 -34
- package/src/daemon/conversation-tool-setup.ts +33 -48
- package/src/daemon/conversation.ts +26 -46
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/daemon-skill-host.ts +9 -2
- package/src/daemon/disk-pressure-guard.ts +27 -29
- package/src/daemon/first-greeting.ts +31 -13
- package/src/daemon/handlers/shared.ts +6 -1
- package/src/daemon/lifecycle.ts +12 -12
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/meet-manifest-loader.ts +10 -17
- package/src/daemon/message-types/conversations.ts +20 -22
- package/src/daemon/message-types/document-comments.ts +8 -44
- package/src/daemon/message-types/home.ts +2 -2
- package/src/daemon/message-types/integrations.ts +2 -7
- package/src/daemon/message-types/messages.ts +23 -38
- package/src/daemon/message-types/subagents.ts +6 -0
- package/src/daemon/process-message.ts +9 -9
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +16 -0
- package/src/daemon/switch-inference-profile-tool.ts +13 -3
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/wake-target-adapter.ts +10 -0
- package/src/documents/document-store.ts +38 -0
- package/src/export/__tests__/transcript-formatter.test.ts +1 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +29 -0
- package/src/heartbeat/heartbeat-service.ts +63 -0
- package/src/home/__tests__/feed-writer.test.ts +161 -0
- package/src/home/__tests__/post-connect-feed.test.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +55 -59
- package/src/home/feed-writer.ts +146 -7
- package/src/home/suggested-prompts.ts +27 -145
- package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
- package/src/ipc/gateway-client.test.ts +4 -1
- package/src/ipc/skill-routes/__tests__/memory.test.ts +1 -0
- package/src/ipc/skill-routes/__tests__/registries.test.ts +36 -7
- package/src/ipc/skill-routes/memory.ts +4 -3
- package/src/ipc/skill-routes/registries.ts +28 -29
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +1 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
- package/src/memory/conversation-attention-store.ts +17 -3
- package/src/memory/conversation-crud.ts +352 -112
- package/src/memory/db-connection.ts +29 -19
- package/src/memory/db-init.ts +4 -0
- package/src/memory/db-singleton.ts +77 -0
- package/src/memory/delivery-channels.ts +82 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
- package/src/memory/graph/retriever.test.ts +3 -3
- package/src/memory/job-handlers/embedding.test.ts +3 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
- package/src/memory/jobs-worker.ts +12 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +80 -0
- package/src/memory/llm-request-log-source-local.ts +24 -0
- package/src/memory/llm-request-log-source.ts +31 -0
- package/src/memory/llm-request-log-store.ts +188 -3
- package/src/memory/memory-v2-activation-log-store.ts +95 -1
- package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
- package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema/conversations.ts +9 -1
- package/src/memory/schema/inference.ts +0 -1
- package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
- package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
- package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
- package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
- package/src/memory/v2/__tests__/sweep-job.test.ts +6 -3
- package/src/memory/v2/harness/metrics.ts +5 -1
- package/src/memory/v2/harness/replay-input.ts +19 -3
- package/src/memory/v2/harness/runner.ts +6 -0
- package/src/memory/v2/harness/trace.ts +6 -0
- package/src/memory/v3/__tests__/consolidation-job.test.ts +2 -4
- package/src/memory/v3/__tests__/coretrieval-seed.test.ts +270 -0
- package/src/memory/v3/__tests__/edges.test.ts +144 -1
- package/src/memory/v3/__tests__/filter.test.ts +48 -0
- package/src/memory/v3/__tests__/gate.test.ts +96 -33
- package/src/memory/v3/__tests__/index-composition.test.ts +58 -0
- package/src/memory/v3/__tests__/loop.test.ts +250 -5
- package/src/memory/v3/__tests__/scouts.test.ts +49 -0
- package/src/memory/v3/__tests__/shadow-diff.test.ts +225 -0
- package/src/memory/v3/__tests__/shadow-middleware.test.ts +88 -2
- package/src/memory/v3/__tests__/traversal.test.ts +39 -0
- package/src/memory/v3/__tests__/tree-walk.test.ts +77 -0
- package/src/memory/v3/__tests__/validate.test.ts +32 -0
- package/src/memory/v3/coretrieval-seed.ts +240 -0
- package/src/memory/v3/edges.ts +58 -21
- package/src/memory/v3/filter.ts +27 -22
- package/src/memory/v3/gate.ts +51 -36
- package/src/memory/v3/index-composition.ts +18 -5
- package/src/memory/v3/loop.ts +65 -17
- package/src/memory/v3/scouts.ts +15 -4
- package/src/memory/v3/shadow-diff.ts +287 -0
- package/src/memory/v3/shadow-middleware.ts +44 -2
- package/src/memory/v3/traversal.ts +6 -1
- package/src/memory/v3/tree-walk.ts +6 -1
- package/src/memory/v3/validate.ts +56 -33
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
- package/src/notifications/adapters/slack.ts +45 -11
- package/src/notifications/broadcaster.ts +114 -63
- package/src/notifications/conversation-pairing.ts +23 -3
- package/src/notifications/decisions-store.ts +32 -1
- package/src/notifications/deliveries-store.ts +45 -0
- package/src/notifications/edit-notification.ts +201 -0
- package/src/notifications/emit-signal.ts +11 -1
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +37 -0
- package/src/oauth/byo-connection.test.ts +67 -3
- package/src/oauth/byo-connection.ts +32 -5
- package/src/oauth/connect-orchestrator.ts +9 -0
- package/src/oauth/connection-resolver.test.ts +76 -0
- package/src/oauth/connection-resolver.ts +49 -10
- package/src/oauth/manual-token-connection.ts +51 -3
- package/src/oauth/seed-providers.ts +3 -0
- package/src/permissions/approval-policy.test.ts +19 -5
- package/src/permissions/approval-policy.ts +14 -3
- package/src/permissions/checker.ts +21 -8
- package/src/platform/client.test.ts +24 -1
- package/src/platform/client.ts +8 -0
- package/src/platform/feature-gate.ts +15 -0
- package/src/plugins/defaults/injectors.ts +2 -8
- package/src/plugins/defaults/persistence.ts +25 -6
- package/src/plugins/types.ts +57 -13
- package/src/proactive-artifact/job.test.ts +1 -0
- package/src/prompts/__tests__/system-prompt.test.ts +4 -4
- package/src/prompts/system-prompt.ts +38 -40
- package/src/prompts/template-detection.ts +10 -4
- package/src/prompts/templates/BOOTSTRAP.md +7 -11
- package/src/prompts/templates/IDENTITY.md +0 -2
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
- package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
- package/src/providers/call-site-routing.ts +33 -9
- package/src/providers/connection-model-compat.ts +23 -0
- package/src/providers/connection-resolution.ts +39 -20
- package/src/providers/fireworks/client.ts +1 -0
- package/src/providers/gemini/client.ts +24 -3
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
- package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
- package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
- package/src/providers/inference/auth.ts +0 -8
- package/src/providers/inference/connections.ts +3 -66
- package/src/providers/inference/resolve-auth.ts +2 -3
- package/src/providers/model-catalog.ts +35 -1
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -5
- package/src/providers/openai/chat-completions-provider.ts +110 -12
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/openai/responses-provider.ts +53 -53
- package/src/providers/openrouter/client.ts +13 -8
- package/src/providers/provider-send-message.ts +18 -9
- package/src/providers/registry.ts +48 -8
- package/src/providers/retry.ts +16 -4
- package/src/providers/search-provider-catalog.ts +17 -9
- package/src/providers/types.ts +9 -0
- package/src/runtime/__tests__/agent-wake.test.ts +1 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +1 -0
- package/src/runtime/access-request-helper.ts +1 -0
- package/src/runtime/auth/route-policy.ts +10 -0
- package/src/runtime/channel-readiness-service.ts +68 -0
- package/src/runtime/channel-reply-delivery.ts +23 -0
- package/src/runtime/channel-retry-sweep.ts +47 -14
- package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
- package/src/runtime/migrations/vbundle-builder.ts +3 -2
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +13 -50
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +51 -3
- package/src/runtime/routes/__tests__/memory-v3-simulate-params.test.ts +35 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
- package/src/runtime/routes/acp-routes-list.test.ts +3 -0
- package/src/runtime/routes/app-management-routes.ts +111 -4
- package/src/runtime/routes/background-wake-routes.ts +188 -20
- package/src/runtime/routes/btw-routes.ts +4 -4
- package/src/runtime/routes/conversation-analysis-routes.ts +6 -0
- package/src/runtime/routes/conversation-compaction-routes.ts +263 -0
- package/src/runtime/routes/conversation-list-routes.ts +147 -0
- package/src/runtime/routes/conversation-management-routes.ts +39 -14
- package/src/runtime/routes/conversation-query-routes.ts +60 -10
- package/src/runtime/routes/conversation-routes.ts +186 -140
- package/src/runtime/routes/conversations-import-routes.ts +19 -6
- package/src/runtime/routes/documents-routes.ts +10 -1
- package/src/runtime/routes/group-routes.ts +11 -0
- package/src/runtime/routes/home-feed-routes.ts +129 -0
- package/src/runtime/routes/identity-intro-cache.ts +61 -16
- package/src/runtime/routes/identity-routes.ts +30 -9
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +530 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +57 -8
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +5 -26
- package/src/runtime/routes/integrations/vercel.ts +15 -0
- package/src/runtime/routes/llm-context-normalization.ts +7 -2
- package/src/runtime/routes/memory-v3-routes.ts +160 -2
- package/src/runtime/routes/migration-routes.ts +20 -13
- package/src/runtime/routes/notification-routes.ts +63 -1
- package/src/runtime/routes/oauth-commands-routes.ts +6 -1
- package/src/runtime/routes/surface-action-routes.ts +1 -38
- package/src/runtime/routes/surface-content-routes.ts +12 -5
- package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
- package/src/runtime/routes/wipe-conversation-routes.ts +3 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -0
- package/src/runtime/slack-dm-text-delivery.ts +177 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/schedule/schedule-store.ts +8 -1
- package/src/schedule/scheduler.ts +111 -15
- package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
- package/src/security/encrypted-store.ts +7 -16
- package/src/security/store-path-override.ts +61 -0
- package/src/signals/user-message.ts +5 -8
- package/src/skills/validate-input.ts +177 -0
- package/src/subagent/manager.ts +13 -13
- package/src/subagent/types.ts +6 -0
- package/src/tasks/tool-sanitizer.ts +2 -2
- package/src/tools/apps/definitions.ts +35 -21
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
- package/src/tools/computer-use/definitions.ts +268 -266
- package/src/tools/document/document-tool.ts +131 -8
- package/src/tools/execution-target.ts +2 -5
- package/src/tools/executor.ts +18 -55
- package/src/tools/host-filesystem/edit.test.ts +1 -0
- package/src/tools/host-filesystem/read.test.ts +1 -0
- package/src/tools/host-filesystem/transfer.test.ts +31 -6
- package/src/tools/host-filesystem/write.test.ts +1 -0
- package/src/tools/mcp/mcp-tool-factory.ts +0 -2
- package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
- package/src/tools/network/__tests__/web-search.test.ts +211 -3
- package/src/tools/network/managed-search-proxy.ts +183 -0
- package/src/tools/network/web-search.ts +199 -44
- package/src/tools/policy-context.ts +3 -1
- package/src/tools/registry.ts +146 -103
- package/src/tools/schedule/create.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +17 -36
- package/src/tools/subagent/spawn.ts +3 -0
- package/src/tools/tool-approval-handler.ts +10 -4
- package/src/tools/tool-name-aliases.ts +72 -14
- package/src/tools/types.ts +17 -15
- package/src/tools/ui-surface/definitions.ts +98 -86
- package/src/types/onboarding-context.ts +6 -0
- package/src/usage/attribution.ts +32 -1
- package/src/util/browser.ts +7 -2
- package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
- package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -109,7 +109,7 @@ export function projectAssistantMessage(params: {
|
|
|
109
109
|
conversationId: string;
|
|
110
110
|
messageId: string;
|
|
111
111
|
messageAt: number;
|
|
112
|
-
}):
|
|
112
|
+
}): boolean {
|
|
113
113
|
const { conversationId, messageId, messageAt } = params;
|
|
114
114
|
const db = getDb();
|
|
115
115
|
const now = Date.now();
|
|
@@ -140,7 +140,7 @@ export function projectAssistantMessage(params: {
|
|
|
140
140
|
updatedAt: now,
|
|
141
141
|
})
|
|
142
142
|
.run();
|
|
143
|
-
return;
|
|
143
|
+
return true;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Monotonic: only advance if the new message is strictly later
|
|
@@ -148,9 +148,21 @@ export function projectAssistantMessage(params: {
|
|
|
148
148
|
existing.latestAssistantMessageAt != null &&
|
|
149
149
|
messageAt <= existing.latestAssistantMessageAt
|
|
150
150
|
) {
|
|
151
|
-
return;
|
|
151
|
+
return false;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// Determine whether the conversation was previously in a "seen" state.
|
|
155
|
+
// Two cases count as seen:
|
|
156
|
+
// 1. latestAssistantMessageAt is null — no prior assistant message existed,
|
|
157
|
+
// so there was nothing unseen. The first assistant message transitions
|
|
158
|
+
// the conversation to unseen.
|
|
159
|
+
// 2. lastSeenAssistantMessageAt >= latestAssistantMessageAt — the user saw
|
|
160
|
+
// the most recent assistant message.
|
|
161
|
+
const wasSeen =
|
|
162
|
+
existing.latestAssistantMessageAt == null ||
|
|
163
|
+
(existing.lastSeenAssistantMessageAt != null &&
|
|
164
|
+
existing.lastSeenAssistantMessageAt >= existing.latestAssistantMessageAt);
|
|
165
|
+
|
|
154
166
|
db.update(conversationAssistantAttentionState)
|
|
155
167
|
.set({
|
|
156
168
|
latestAssistantMessageId: messageId,
|
|
@@ -161,6 +173,8 @@ export function projectAssistantMessage(params: {
|
|
|
161
173
|
eq(conversationAssistantAttentionState.conversationId, conversationId),
|
|
162
174
|
)
|
|
163
175
|
.run();
|
|
176
|
+
|
|
177
|
+
return wasSeen;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
/**
|
|
@@ -25,7 +25,10 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
|
|
|
25
25
|
import { CHANNEL_IDS, isChannelId } from "../channels/types.js";
|
|
26
26
|
import { getConfig } from "../config/loader.js";
|
|
27
27
|
import { findDisplayTurnEndIndex } from "../conversations/message-consolidation.js";
|
|
28
|
+
import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
|
|
28
29
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
30
|
+
import { clearAllConversationIds } from "../home/feed-writer.js";
|
|
31
|
+
import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
|
|
29
32
|
import { UserError } from "../util/errors.js";
|
|
30
33
|
import { safeParseRecord } from "../util/json.js";
|
|
31
34
|
import { getLogger } from "../util/logger.js";
|
|
@@ -181,6 +184,24 @@ export function provenanceFromTrustContext(
|
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
/** Extract image file paths from resolved attachments for message metadata. */
|
|
188
|
+
export function extractImageSourcePaths(
|
|
189
|
+
attachments: ReadonlyArray<{
|
|
190
|
+
filename: string;
|
|
191
|
+
mimeType: string;
|
|
192
|
+
filePath?: string;
|
|
193
|
+
}>,
|
|
194
|
+
): Record<string, string> | undefined {
|
|
195
|
+
const paths: Record<string, string> = {};
|
|
196
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
197
|
+
const a = attachments[i];
|
|
198
|
+
if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
|
|
199
|
+
paths[`${i}:${a.filename}`] = a.filePath;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(paths).length > 0 ? paths : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
184
205
|
export interface ConversationRow {
|
|
185
206
|
id: string;
|
|
186
207
|
title: string | null;
|
|
@@ -246,6 +267,9 @@ export const parseConversation = createRowMapper<
|
|
|
246
267
|
lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
|
|
247
268
|
});
|
|
248
269
|
|
|
270
|
+
/** Allowed values for the `role` column on `messages`. */
|
|
271
|
+
export type MessageRole = "user" | "assistant" | "system";
|
|
272
|
+
|
|
249
273
|
export interface MessageRow {
|
|
250
274
|
id: string;
|
|
251
275
|
conversationId: string;
|
|
@@ -280,6 +304,193 @@ function monotonicNow(): number {
|
|
|
280
304
|
return lastTimestamp;
|
|
281
305
|
}
|
|
282
306
|
|
|
307
|
+
// ── insertMessageCore ─────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
/** Shape returned by {@link insertMessageCore} and its public wrappers. */
|
|
310
|
+
interface InsertedMessage {
|
|
311
|
+
id: string;
|
|
312
|
+
conversationId: string;
|
|
313
|
+
role: MessageRole;
|
|
314
|
+
content: string;
|
|
315
|
+
createdAt: number;
|
|
316
|
+
metadata?: string;
|
|
317
|
+
clientMessageId?: string;
|
|
318
|
+
deduplicated: boolean;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface InsertMessageCoreParams {
|
|
322
|
+
conversationId: string;
|
|
323
|
+
role: MessageRole;
|
|
324
|
+
content: string;
|
|
325
|
+
metadata?: Record<string, unknown>;
|
|
326
|
+
clientMessageId?: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Core message persistence primitive shared by {@link addMessage} and
|
|
331
|
+
* {@link reserveMessage}.
|
|
332
|
+
*
|
|
333
|
+
* Inserts a message row inside a transaction that atomically bumps the
|
|
334
|
+
* parent conversation's `updatedAt` / `lastMessageAt` timestamps and
|
|
335
|
+
* conditionally sets the conversation's `originChannel` when the first
|
|
336
|
+
* channel-originated message arrives.
|
|
337
|
+
*
|
|
338
|
+
* When a `clientMessageId` is provided the insert runs inside a
|
|
339
|
+
* SAVEPOINT. If the partial unique index on
|
|
340
|
+
* `(conversation_id, client_message_id)` raises
|
|
341
|
+
* `SQLITE_CONSTRAINT_UNIQUE`, the SAVEPOINT is rolled back, the
|
|
342
|
+
* existing row is fetched, and returned with `deduplicated: true`.
|
|
343
|
+
* This makes the operation idempotent for client-generated
|
|
344
|
+
* correlation nonces.
|
|
345
|
+
*
|
|
346
|
+
* Retries up to 3 times on `SQLITE_BUSY*` / `SQLITE_IOERR*` to handle
|
|
347
|
+
* WAL contention. The timestamp is recomputed each attempt so a late
|
|
348
|
+
* retry doesn't persist a stale `updatedAt`.
|
|
349
|
+
*/
|
|
350
|
+
async function insertMessageCore(
|
|
351
|
+
params: InsertMessageCoreParams,
|
|
352
|
+
): Promise<InsertedMessage> {
|
|
353
|
+
const { conversationId, role, content, metadata, clientMessageId } = params;
|
|
354
|
+
const db = getDb();
|
|
355
|
+
const messageId = uuid();
|
|
356
|
+
|
|
357
|
+
if (metadata) {
|
|
358
|
+
const result = messageMetadataSchema.safeParse(metadata);
|
|
359
|
+
if (!result.success) {
|
|
360
|
+
log.warn(
|
|
361
|
+
{ conversationId, messageId, issues: result.error.issues },
|
|
362
|
+
"Invalid message metadata, storing as-is",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const metadataStr = metadata ? JSON.stringify(metadata) : undefined;
|
|
368
|
+
const originChannelCandidate =
|
|
369
|
+
metadata && isChannelId(metadata.userMessageChannel)
|
|
370
|
+
? metadata.userMessageChannel
|
|
371
|
+
: null;
|
|
372
|
+
|
|
373
|
+
const MAX_RETRIES = 3;
|
|
374
|
+
let now!: number;
|
|
375
|
+
for (let attempt = 0; ; attempt++) {
|
|
376
|
+
now = monotonicNow();
|
|
377
|
+
try {
|
|
378
|
+
const values = {
|
|
379
|
+
id: messageId,
|
|
380
|
+
conversationId,
|
|
381
|
+
role,
|
|
382
|
+
content,
|
|
383
|
+
createdAt: now,
|
|
384
|
+
...(metadataStr ? { metadata: metadataStr } : {}),
|
|
385
|
+
...(clientMessageId ? { clientMessageId } : {}),
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (clientMessageId) {
|
|
389
|
+
// Idempotent insert: skip silently if this clientMessageId was
|
|
390
|
+
// already persisted for the conversation.
|
|
391
|
+
const raw = getSqliteFrom(db);
|
|
392
|
+
raw.exec("SAVEPOINT insert_msg");
|
|
393
|
+
try {
|
|
394
|
+
db.insert(messages).values(values).run();
|
|
395
|
+
if (originChannelCandidate) {
|
|
396
|
+
db.update(conversations)
|
|
397
|
+
.set({ originChannel: originChannelCandidate })
|
|
398
|
+
.where(
|
|
399
|
+
and(
|
|
400
|
+
eq(conversations.id, conversationId),
|
|
401
|
+
isNull(conversations.originChannel),
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
.run();
|
|
405
|
+
}
|
|
406
|
+
db.update(conversations)
|
|
407
|
+
.set({ updatedAt: now, lastMessageAt: now })
|
|
408
|
+
.where(eq(conversations.id, conversationId))
|
|
409
|
+
.run();
|
|
410
|
+
raw.exec("RELEASE insert_msg");
|
|
411
|
+
} catch (insertErr) {
|
|
412
|
+
raw.exec("ROLLBACK TO insert_msg");
|
|
413
|
+
raw.exec("RELEASE insert_msg");
|
|
414
|
+
const code = (insertErr as { code?: string }).code ?? "";
|
|
415
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
416
|
+
// Duplicate clientMessageId — return the existing row.
|
|
417
|
+
const existing = db
|
|
418
|
+
.select()
|
|
419
|
+
.from(messages)
|
|
420
|
+
.where(
|
|
421
|
+
and(
|
|
422
|
+
eq(messages.conversationId, conversationId),
|
|
423
|
+
eq(messages.clientMessageId, clientMessageId),
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
.get();
|
|
427
|
+
if (existing) {
|
|
428
|
+
return {
|
|
429
|
+
id: existing.id,
|
|
430
|
+
conversationId: existing.conversationId,
|
|
431
|
+
role: existing.role as MessageRole,
|
|
432
|
+
content: existing.content,
|
|
433
|
+
createdAt: existing.createdAt,
|
|
434
|
+
...(existing.metadata ? { metadata: existing.metadata } : {}),
|
|
435
|
+
clientMessageId: existing.clientMessageId ?? undefined,
|
|
436
|
+
deduplicated: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
throw insertErr;
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
// No clientMessageId — standard insert inside a transaction.
|
|
444
|
+
db.transaction((tx) => {
|
|
445
|
+
tx.insert(messages).values(values).run();
|
|
446
|
+
if (originChannelCandidate) {
|
|
447
|
+
tx.update(conversations)
|
|
448
|
+
.set({ originChannel: originChannelCandidate })
|
|
449
|
+
.where(
|
|
450
|
+
and(
|
|
451
|
+
eq(conversations.id, conversationId),
|
|
452
|
+
isNull(conversations.originChannel),
|
|
453
|
+
),
|
|
454
|
+
)
|
|
455
|
+
.run();
|
|
456
|
+
}
|
|
457
|
+
tx.update(conversations)
|
|
458
|
+
.set({ updatedAt: now, lastMessageAt: now })
|
|
459
|
+
.where(eq(conversations.id, conversationId))
|
|
460
|
+
.run();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
} catch (err) {
|
|
465
|
+
const errCode = (err as { code?: string }).code ?? "";
|
|
466
|
+
if (
|
|
467
|
+
attempt < MAX_RETRIES &&
|
|
468
|
+
(errCode.startsWith("SQLITE_BUSY") ||
|
|
469
|
+
errCode.startsWith("SQLITE_IOERR"))
|
|
470
|
+
) {
|
|
471
|
+
log.warn(
|
|
472
|
+
{ attempt, conversationId, code: errCode },
|
|
473
|
+
"insertMessageCore: transient SQLite error, retrying",
|
|
474
|
+
);
|
|
475
|
+
await Bun.sleep(50 * (attempt + 1));
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
throw err;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
id: messageId,
|
|
484
|
+
conversationId,
|
|
485
|
+
role,
|
|
486
|
+
content,
|
|
487
|
+
createdAt: now,
|
|
488
|
+
...(metadataStr ? { metadata: metadataStr } : {}),
|
|
489
|
+
...(clientMessageId ? { clientMessageId } : {}),
|
|
490
|
+
deduplicated: false,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
283
494
|
export function createConversation(
|
|
284
495
|
titleOrOpts?:
|
|
285
496
|
| string
|
|
@@ -576,6 +787,20 @@ export function forkConversation(params: {
|
|
|
576
787
|
throw new UserError(`Conversation ${conversationId} not found`);
|
|
577
788
|
}
|
|
578
789
|
const sourceMessages = getMessages(conversationId);
|
|
790
|
+
if (throughMessageId != null) {
|
|
791
|
+
// `getMessages` orders by `createdAt` only; when rows share an identical
|
|
792
|
+
// millisecond timestamp the tie order is unspecified. Callers that pin the
|
|
793
|
+
// fork to a cutoff choose it from a `(createdAt, id)` cursor (e.g. the
|
|
794
|
+
// memory-retrospective job, via `getMessagesAfter`), so slicing through
|
|
795
|
+
// `throughMessageId` under the unstable order could include same-timestamp
|
|
796
|
+
// siblings the cursor considers *after* the cutoff (reprocessed next run)
|
|
797
|
+
// or exclude ones it considers *before* it (skipped forever). Re-sort on
|
|
798
|
+
// `(createdAt, id)` so the slice agrees with the cutoff. The unpinned full
|
|
799
|
+
// fork copies every row regardless of order, so it keeps source order.
|
|
800
|
+
sourceMessages.sort(
|
|
801
|
+
(a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id),
|
|
802
|
+
);
|
|
803
|
+
}
|
|
579
804
|
|
|
580
805
|
if (sourceMessages.length === 0) {
|
|
581
806
|
throw new UserError(
|
|
@@ -970,92 +1195,36 @@ export function wipeConversation(id: string): WipeConversationResult {
|
|
|
970
1195
|
};
|
|
971
1196
|
}
|
|
972
1197
|
|
|
1198
|
+
/**
|
|
1199
|
+
* Persist a message and run post-insert side effects (memory indexing,
|
|
1200
|
+
* attention projection). Delegates the core insert + retry logic to
|
|
1201
|
+
* {@link insertMessageCore}.
|
|
1202
|
+
*
|
|
1203
|
+
* @param clientMessageId Optional client-generated nonce. When
|
|
1204
|
+
* provided, duplicate inserts for the same `(conversationId,
|
|
1205
|
+
* clientMessageId)` pair are silently skipped (idempotent).
|
|
1206
|
+
*/
|
|
973
1207
|
export async function addMessage(
|
|
974
1208
|
conversationId: string,
|
|
975
|
-
role:
|
|
1209
|
+
role: MessageRole,
|
|
976
1210
|
content: string,
|
|
977
1211
|
metadata?: Record<string, unknown>,
|
|
978
1212
|
opts?: { skipIndexing?: boolean },
|
|
1213
|
+
clientMessageId?: string,
|
|
979
1214
|
) {
|
|
980
|
-
const
|
|
981
|
-
const messageId = uuid();
|
|
982
|
-
|
|
983
|
-
if (metadata) {
|
|
984
|
-
const result = messageMetadataSchema.safeParse(metadata);
|
|
985
|
-
if (!result.success) {
|
|
986
|
-
log.warn(
|
|
987
|
-
{ conversationId, messageId, issues: result.error.issues },
|
|
988
|
-
"Invalid message metadata, storing as-is",
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
const metadataStr = metadata ? JSON.stringify(metadata) : undefined;
|
|
994
|
-
const originChannelCandidate =
|
|
995
|
-
metadata && isChannelId(metadata.userMessageChannel)
|
|
996
|
-
? metadata.userMessageChannel
|
|
997
|
-
: null;
|
|
998
|
-
// Wrap insert + updatedAt bump in a transaction so they're atomic.
|
|
999
|
-
// Retry on SQLITE_BUSY* and SQLITE_IOERR* — covers WAL contention variants
|
|
1000
|
-
// (SQLITE_BUSY_SNAPSHOT, SQLITE_BUSY_RECOVERY) and transient disk I/O errors.
|
|
1001
|
-
// Timestamp is recomputed each attempt so a late retry doesn't persist a stale updatedAt.
|
|
1002
|
-
const MAX_RETRIES = 3;
|
|
1003
|
-
let now!: number;
|
|
1004
|
-
for (let attempt = 0; ; attempt++) {
|
|
1005
|
-
now = monotonicNow();
|
|
1006
|
-
try {
|
|
1007
|
-
const values = {
|
|
1008
|
-
id: messageId,
|
|
1009
|
-
conversationId,
|
|
1010
|
-
role,
|
|
1011
|
-
content,
|
|
1012
|
-
createdAt: now,
|
|
1013
|
-
...(metadataStr ? { metadata: metadataStr } : {}),
|
|
1014
|
-
};
|
|
1015
|
-
db.transaction((tx) => {
|
|
1016
|
-
tx.insert(messages).values(values).run();
|
|
1017
|
-
if (originChannelCandidate) {
|
|
1018
|
-
tx.update(conversations)
|
|
1019
|
-
.set({ originChannel: originChannelCandidate })
|
|
1020
|
-
.where(
|
|
1021
|
-
and(
|
|
1022
|
-
eq(conversations.id, conversationId),
|
|
1023
|
-
isNull(conversations.originChannel),
|
|
1024
|
-
),
|
|
1025
|
-
)
|
|
1026
|
-
.run();
|
|
1027
|
-
}
|
|
1028
|
-
tx.update(conversations)
|
|
1029
|
-
.set({ updatedAt: now, lastMessageAt: now })
|
|
1030
|
-
.where(eq(conversations.id, conversationId))
|
|
1031
|
-
.run();
|
|
1032
|
-
});
|
|
1033
|
-
break;
|
|
1034
|
-
} catch (err) {
|
|
1035
|
-
const errCode = (err as { code?: string }).code ?? "";
|
|
1036
|
-
if (
|
|
1037
|
-
attempt < MAX_RETRIES &&
|
|
1038
|
-
(errCode.startsWith("SQLITE_BUSY") ||
|
|
1039
|
-
errCode.startsWith("SQLITE_IOERR"))
|
|
1040
|
-
) {
|
|
1041
|
-
log.warn(
|
|
1042
|
-
{ attempt, conversationId, code: errCode },
|
|
1043
|
-
"addMessage: transient SQLite error, retrying",
|
|
1044
|
-
);
|
|
1045
|
-
await Bun.sleep(50 * (attempt + 1));
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
throw err;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
const message = {
|
|
1052
|
-
id: messageId,
|
|
1215
|
+
const inserted = await insertMessageCore({
|
|
1053
1216
|
conversationId,
|
|
1054
1217
|
role,
|
|
1055
1218
|
content,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
};
|
|
1219
|
+
metadata,
|
|
1220
|
+
clientMessageId,
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
if (inserted.deduplicated) {
|
|
1224
|
+
return inserted;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const message = inserted;
|
|
1059
1228
|
|
|
1060
1229
|
if (!opts?.skipIndexing) {
|
|
1061
1230
|
try {
|
|
@@ -1090,11 +1259,16 @@ export async function addMessage(
|
|
|
1090
1259
|
|
|
1091
1260
|
if (role === "assistant") {
|
|
1092
1261
|
try {
|
|
1093
|
-
projectAssistantMessage({
|
|
1262
|
+
const attentionStateChanged = projectAssistantMessage({
|
|
1094
1263
|
conversationId,
|
|
1095
1264
|
messageId: message.id,
|
|
1096
1265
|
messageAt: message.createdAt,
|
|
1097
1266
|
});
|
|
1267
|
+
if (attentionStateChanged) {
|
|
1268
|
+
void publishSyncInvalidation([
|
|
1269
|
+
conversationMetadataSyncTag(conversationId),
|
|
1270
|
+
]);
|
|
1271
|
+
}
|
|
1098
1272
|
} catch (err) {
|
|
1099
1273
|
log.warn(
|
|
1100
1274
|
{ err, conversationId, messageId: message.id },
|
|
@@ -1288,11 +1462,30 @@ export function hasMessages(conversationId: string): boolean {
|
|
|
1288
1462
|
interface PaginatedMessagesResult {
|
|
1289
1463
|
messages: MessageRow[];
|
|
1290
1464
|
hasMore: boolean;
|
|
1465
|
+
/**
|
|
1466
|
+
* Position of the last row scanned when the loop stops on
|
|
1467
|
+
* `PAGINATION_SCAN_CAP` rather than DB exhaustion. Callers derive their
|
|
1468
|
+
* client cursor from the visible page's oldest row, but a cap-truncated
|
|
1469
|
+
* page can be empty (a contiguous block of filtered-out rows longer than
|
|
1470
|
+
* the cap), leaving nothing to resume from. Surfacing the last scanned
|
|
1471
|
+
* `(createdAt, id)` lets the caller hand the client a cursor so it can
|
|
1472
|
+
* request the next window and keep draining instead of stalling.
|
|
1473
|
+
*/
|
|
1474
|
+
nextCursor?: { createdAt: number; id: string };
|
|
1291
1475
|
}
|
|
1292
1476
|
|
|
1293
1477
|
const PAGINATION_CHUNK_MIN = 50;
|
|
1294
1478
|
const PAGINATION_SCAN_CAP = 10_000;
|
|
1295
1479
|
|
|
1480
|
+
// Test-only override for PAGINATION_SCAN_CAP so tests can exercise the
|
|
1481
|
+
// cap-truncation branch with a small cap instead of seeding >10k rows (which
|
|
1482
|
+
// makes the suite slow and the post-test DELETE flaky under parallel CI load).
|
|
1483
|
+
// `undefined` restores the production cap.
|
|
1484
|
+
let paginationScanCapOverride: number | undefined;
|
|
1485
|
+
export function _setPaginationScanCapForTesting(cap: number | undefined): void {
|
|
1486
|
+
paginationScanCapOverride = cap;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1296
1489
|
export function getMessagesPaginated(
|
|
1297
1490
|
conversationId: string,
|
|
1298
1491
|
limit: number | undefined,
|
|
@@ -1333,8 +1526,19 @@ export function getMessagesPaginated(
|
|
|
1333
1526
|
// row — otherwise a pathological filter against a huge conversation would
|
|
1334
1527
|
// tie up a connection for thousands of roundtrips.
|
|
1335
1528
|
let rowsScanned = 0;
|
|
1336
|
-
|
|
1337
|
-
|
|
1529
|
+
// Distinguish "stopped because we hit the scan cap" from "stopped because the
|
|
1530
|
+
// DB ran out of rows". On a cap-truncated stop there may be more visible rows
|
|
1531
|
+
// past the scanned window, so `hasMore` must stay true and we record the last
|
|
1532
|
+
// scanned position as a resume cursor (the visible page may be empty).
|
|
1533
|
+
let scanCapTruncated = false;
|
|
1534
|
+
let lastScanned: { createdAt: number; id: string } | undefined;
|
|
1535
|
+
const scanCap = paginationScanCapOverride ?? PAGINATION_SCAN_CAP;
|
|
1536
|
+
|
|
1537
|
+
while (visible.length < limit + 1) {
|
|
1538
|
+
if (rowsScanned >= scanCap) {
|
|
1539
|
+
scanCapTruncated = true;
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1338
1542
|
const cursorPredicate =
|
|
1339
1543
|
cursorCreatedAt === undefined
|
|
1340
1544
|
? undefined
|
|
@@ -1367,15 +1571,25 @@ export function getMessagesPaginated(
|
|
|
1367
1571
|
|
|
1368
1572
|
if (chunk.length < chunkSize) break;
|
|
1369
1573
|
const lastRow = chunk[chunk.length - 1];
|
|
1574
|
+
lastScanned = { createdAt: lastRow.createdAt, id: lastRow.id };
|
|
1370
1575
|
cursorCreatedAt = lastRow.createdAt;
|
|
1371
1576
|
cursorMessageId = lastRow.id;
|
|
1372
1577
|
}
|
|
1373
1578
|
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1579
|
+
const filledPage = visible.length > limit;
|
|
1580
|
+
// A cap-truncated stop means the DB may still hold older visible rows past
|
|
1581
|
+
// the scanned window, so report `hasMore: true` to keep the client draining
|
|
1582
|
+
// — returning `false` here is the stall this loop exists to prevent.
|
|
1583
|
+
const hasMore = filledPage || scanCapTruncated;
|
|
1584
|
+
if (filledPage) visible.splice(limit);
|
|
1376
1585
|
visible.reverse();
|
|
1377
1586
|
|
|
1378
|
-
|
|
1587
|
+
// Only hand back a resume cursor when the cap (not DB exhaustion) cut the
|
|
1588
|
+
// search short; callers fall back to it when the visible page came back
|
|
1589
|
+
// empty and has no oldest row to anchor the next request.
|
|
1590
|
+
const nextCursor = scanCapTruncated ? lastScanned : undefined;
|
|
1591
|
+
|
|
1592
|
+
return { messages: visible, hasMore, nextCursor };
|
|
1379
1593
|
}
|
|
1380
1594
|
|
|
1381
1595
|
export function getLastUserTimestampBefore(
|
|
@@ -1821,6 +2035,8 @@ export async function clearAll(): Promise<{
|
|
|
1821
2035
|
log.warn({ err }, "clearAll: failed to reset conversations directory");
|
|
1822
2036
|
}
|
|
1823
2037
|
|
|
2038
|
+
void clearAllConversationIds();
|
|
2039
|
+
|
|
1824
2040
|
return { conversations: convCount, messages: msgCount };
|
|
1825
2041
|
}
|
|
1826
2042
|
|
|
@@ -1913,6 +2129,29 @@ interface WipeConversationResult extends DeletedMemoryIds {
|
|
|
1913
2129
|
cancelledJobCount: number;
|
|
1914
2130
|
}
|
|
1915
2131
|
|
|
2132
|
+
/**
|
|
2133
|
+
* Reserve an empty message row so the agent loop can stamp outbound
|
|
2134
|
+
* streaming events with a stable identity before content is produced.
|
|
2135
|
+
*
|
|
2136
|
+
* Intentionally skips Qdrant indexing and attention projection — an empty
|
|
2137
|
+
* placeholder is meaningless for either. The caller writes final content
|
|
2138
|
+
* via {@link updateMessageContent} and handles indexing/projection itself.
|
|
2139
|
+
*
|
|
2140
|
+
* Delegates the core insert + retry logic to {@link insertMessageCore}.
|
|
2141
|
+
*/
|
|
2142
|
+
export async function reserveMessage(
|
|
2143
|
+
conversationId: string,
|
|
2144
|
+
role: MessageRole,
|
|
2145
|
+
metadata?: Record<string, unknown>,
|
|
2146
|
+
) {
|
|
2147
|
+
return insertMessageCore({
|
|
2148
|
+
conversationId,
|
|
2149
|
+
role,
|
|
2150
|
+
content: "[]",
|
|
2151
|
+
metadata,
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1916
2155
|
/**
|
|
1917
2156
|
* Update the content of an existing message. Used when consolidating
|
|
1918
2157
|
* multiple assistant messages into one.
|
|
@@ -2186,7 +2425,7 @@ export function batchSetDisplayOrders(
|
|
|
2186
2425
|
updates: Array<{
|
|
2187
2426
|
id: string;
|
|
2188
2427
|
displayOrder: number | null;
|
|
2189
|
-
isPinned
|
|
2428
|
+
isPinned?: boolean;
|
|
2190
2429
|
groupId?: string | null;
|
|
2191
2430
|
}>,
|
|
2192
2431
|
): void {
|
|
@@ -2219,35 +2458,36 @@ export function batchSetDisplayOrders(
|
|
|
2219
2458
|
safeGroupId,
|
|
2220
2459
|
update.id,
|
|
2221
2460
|
);
|
|
2461
|
+
} else if (update.isPinned === undefined) {
|
|
2462
|
+
// Only displayOrder provided — preserve existing pin state and group.
|
|
2463
|
+
rawRun(
|
|
2464
|
+
"UPDATE conversations SET display_order = ? WHERE id = ?",
|
|
2465
|
+
update.displayOrder,
|
|
2466
|
+
update.id,
|
|
2467
|
+
);
|
|
2468
|
+
} else if (update.isPinned) {
|
|
2469
|
+
rawRun(
|
|
2470
|
+
"UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
|
|
2471
|
+
update.displayOrder,
|
|
2472
|
+
update.id,
|
|
2473
|
+
);
|
|
2222
2474
|
} else {
|
|
2223
|
-
//
|
|
2224
|
-
//
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
CASE
|
|
2240
|
-
WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
|
|
2241
|
-
WHEN source IN ('heartbeat', 'task') THEN 'system:background'
|
|
2242
|
-
WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
|
|
2243
|
-
ELSE 'system:all'
|
|
2244
|
-
END
|
|
2245
|
-
ELSE group_id END
|
|
2246
|
-
WHERE id = ?`,
|
|
2247
|
-
update.displayOrder,
|
|
2248
|
-
update.id,
|
|
2249
|
-
);
|
|
2250
|
-
}
|
|
2475
|
+
// Restore system group from source/conversationType when unpinning,
|
|
2476
|
+
// instead of clearing to NULL (which would lose provenance).
|
|
2477
|
+
rawRun(
|
|
2478
|
+
`UPDATE conversations SET display_order = ?, is_pinned = 0,
|
|
2479
|
+
group_id = CASE WHEN group_id = 'system:pinned' THEN
|
|
2480
|
+
CASE
|
|
2481
|
+
WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
|
|
2482
|
+
WHEN source IN ('heartbeat', 'task') THEN 'system:background'
|
|
2483
|
+
WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
|
|
2484
|
+
ELSE 'system:all'
|
|
2485
|
+
END
|
|
2486
|
+
ELSE group_id END
|
|
2487
|
+
WHERE id = ?`,
|
|
2488
|
+
update.displayOrder,
|
|
2489
|
+
update.id,
|
|
2490
|
+
);
|
|
2251
2491
|
}
|
|
2252
2492
|
}
|
|
2253
2493
|
rawExec("COMMIT");
|