@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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory v3 — co-retrieval edge seeding.
|
|
3
|
+
*
|
|
4
|
+
* Warm-starts the learned association graph (`memory_v3_auto_edges`, migration
|
|
5
|
+
* 263) from the v2 router's selection history. The live edge-learning job
|
|
6
|
+
* (`edge-learning-job.ts`) only accrues edges from v3's own retrievals, which is
|
|
7
|
+
* cold until v3 has run for a long time; the v2 router has thousands of turns of
|
|
8
|
+
* co-selection data already. Seeding projects that history into the same table
|
|
9
|
+
* the `aboveThreshold` read path already consumes, so the edge-expansion lane
|
|
10
|
+
* can merge it (via `expandEdges`'s `extraAdjacency` seam) the moment it's wired.
|
|
11
|
+
*
|
|
12
|
+
* Signal: two pages that the router *selected together* on a turn are associated.
|
|
13
|
+
* Scoring is **NPMI** (normalized pointwise mutual information), not raw
|
|
14
|
+
* co-occurrence — NPMI discounts high-frequency pages, so an always-injected page
|
|
15
|
+
* doesn't edge to everything it merely co-occurs with. A min co-occurrence floor
|
|
16
|
+
* drops noise, and an "always-on" frequency ceiling drops pages selected on a
|
|
17
|
+
* large fraction of all turns (heartbeat/now-md-style infrastructure) that carry
|
|
18
|
+
* no associative signal.
|
|
19
|
+
*
|
|
20
|
+
* `buildCoretrievalGraph` is pure (no I/O) so it is unit-testable; the
|
|
21
|
+
* `seedCoretrievalEdges` driver reads the rows and writes the table.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getLogger } from "../../util/logger.js";
|
|
25
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
26
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
27
|
+
|
|
28
|
+
const log = getLogger("memory-v3-coretrieval-seed");
|
|
29
|
+
|
|
30
|
+
/** A pair must co-occur on at least this many turns to earn an edge. */
|
|
31
|
+
export const DEFAULT_MIN_COUNT = 5;
|
|
32
|
+
/**
|
|
33
|
+
* A candidate *neighbor* selected on more than this fraction of all turns is
|
|
34
|
+
* "always-on" infrastructure (co-occurs with everything) and is excluded — it
|
|
35
|
+
* carries no associative signal, only base-rate noise.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_MAX_NEIGHBOR_FREQ_RATIO = 0.4;
|
|
38
|
+
/** Neighbors kept per source node, ranked by NPMI descending. */
|
|
39
|
+
export const DEFAULT_TOP_K = 20;
|
|
40
|
+
/**
|
|
41
|
+
* Weight of a perfect-association (NPMI = 1) edge. Each edge is persisted at
|
|
42
|
+
* `seedWeight × its NPMI score`, so the read threshold
|
|
43
|
+
* (`memory.v3.edges.learnedAdjacencyThreshold`) acts as a minimum-association
|
|
44
|
+
* cutoff: with the default 2.0, threshold 1.0 ≈ NPMI ≥ 0.5 and 1.2 ≈ NPMI ≥ 0.6.
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_SEED_WEIGHT = 2.0;
|
|
47
|
+
|
|
48
|
+
/** Tuning knobs for {@link buildCoretrievalGraph}. */
|
|
49
|
+
export interface CoretrievalGraphOptions {
|
|
50
|
+
minCount: number;
|
|
51
|
+
maxNeighborFreqRatio: number;
|
|
52
|
+
topK: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Driver knobs: graph tuning plus the persisted seed weight. */
|
|
56
|
+
export interface SeedCoretrievalOptions extends CoretrievalGraphOptions {
|
|
57
|
+
seedWeight: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_SEED_OPTIONS: SeedCoretrievalOptions = {
|
|
61
|
+
minCount: DEFAULT_MIN_COUNT,
|
|
62
|
+
maxNeighborFreqRatio: DEFAULT_MAX_NEIGHBOR_FREQ_RATIO,
|
|
63
|
+
topK: DEFAULT_TOP_K,
|
|
64
|
+
seedWeight: DEFAULT_SEED_WEIGHT,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** One scored outgoing edge. */
|
|
68
|
+
export interface ScoredEdge {
|
|
69
|
+
target: string;
|
|
70
|
+
score: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Summary of one seeding run, for the CLI/route report. */
|
|
74
|
+
export interface SeedCoretrievalResult {
|
|
75
|
+
turnsScanned: number;
|
|
76
|
+
nodes: number;
|
|
77
|
+
edgesWritten: number;
|
|
78
|
+
avgDegree: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the co-retrieval adjacency from per-turn selected-slug sets.
|
|
83
|
+
*
|
|
84
|
+
* Pure: takes the already-extracted selection sets (one array per turn) and
|
|
85
|
+
* returns `source -> ScoredEdge[]` (top-K NPMI neighbors, heaviest first). Turns
|
|
86
|
+
* with fewer than two distinct slugs contribute no pairs and are ignored.
|
|
87
|
+
*/
|
|
88
|
+
export function buildCoretrievalGraph(
|
|
89
|
+
turns: ReadonlyArray<ReadonlyArray<string>>,
|
|
90
|
+
options: CoretrievalGraphOptions = DEFAULT_SEED_OPTIONS,
|
|
91
|
+
): Map<string, ScoredEdge[]> {
|
|
92
|
+
const sets = turns.map((t) => [...new Set(t)]).filter((s) => s.length >= 2);
|
|
93
|
+
const n = sets.length;
|
|
94
|
+
const graph = new Map<string, ScoredEdge[]>();
|
|
95
|
+
if (n === 0) return graph;
|
|
96
|
+
|
|
97
|
+
const freq = new Map<string, number>();
|
|
98
|
+
const cooccur = new Map<string, Map<string, number>>();
|
|
99
|
+
const bump = (a: string, b: string) => {
|
|
100
|
+
let row = cooccur.get(a);
|
|
101
|
+
if (!row) cooccur.set(a, (row = new Map()));
|
|
102
|
+
row.set(b, (row.get(b) ?? 0) + 1);
|
|
103
|
+
};
|
|
104
|
+
for (const slugs of sets) {
|
|
105
|
+
for (const slug of slugs) freq.set(slug, (freq.get(slug) ?? 0) + 1);
|
|
106
|
+
for (let i = 0; i < slugs.length; i++) {
|
|
107
|
+
for (let j = i + 1; j < slugs.length; j++) {
|
|
108
|
+
bump(slugs[i], slugs[j]);
|
|
109
|
+
bump(slugs[j], slugs[i]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const freqCap = options.maxNeighborFreqRatio * n;
|
|
115
|
+
for (const [source, neighbors] of cooccur) {
|
|
116
|
+
const fSource = freq.get(source)!;
|
|
117
|
+
const scored: ScoredEdge[] = [];
|
|
118
|
+
for (const [target, pairCount] of neighbors) {
|
|
119
|
+
if (pairCount < options.minCount) continue;
|
|
120
|
+
const fTarget = freq.get(target)!;
|
|
121
|
+
if (fTarget > freqCap) continue;
|
|
122
|
+
// NPMI = pmi / -ln(p(a,b)), in [-1, 1]. Higher = stronger association.
|
|
123
|
+
// p(a,b)=1 (the pair co-occurs on every turn) is perfect association — its
|
|
124
|
+
// NPMI limit is 1, but the formula is 0/0, so pin it to avoid NaN.
|
|
125
|
+
const pab = pairCount / n;
|
|
126
|
+
if (pab >= 1) {
|
|
127
|
+
scored.push({ target, score: 1 });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const pmi = Math.log(pab / ((fSource / n) * (fTarget / n)));
|
|
131
|
+
scored.push({ target, score: pmi / -Math.log(pab) });
|
|
132
|
+
}
|
|
133
|
+
scored.sort((a, b) => b.score - a.score);
|
|
134
|
+
if (scored.length > 0) graph.set(source, scored.slice(0, options.topK));
|
|
135
|
+
}
|
|
136
|
+
return graph;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract one selection set per v2 router turn from `memory_v2_activation_logs`.
|
|
141
|
+
* The set is the router's *fresh* per-turn pick (`status === 'injected'`), which
|
|
142
|
+
* is the co-selection signal we want — not the accumulated in-context carry-over.
|
|
143
|
+
*
|
|
144
|
+
* Best-effort: a missing table (db predates the activation log) or an unparseable
|
|
145
|
+
* row yields no turns rather than throwing — the caller degrades to "no seed".
|
|
146
|
+
*/
|
|
147
|
+
function readRouterSelections(database: DrizzleDb): string[][] {
|
|
148
|
+
const turns: string[][] = [];
|
|
149
|
+
try {
|
|
150
|
+
const raw = getSqliteFrom(database);
|
|
151
|
+
const rows = raw
|
|
152
|
+
.query(
|
|
153
|
+
`SELECT concepts_json FROM memory_v2_activation_logs WHERE mode = 'router'`,
|
|
154
|
+
)
|
|
155
|
+
.all() as Array<{ concepts_json: string }>;
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
let concepts: unknown;
|
|
158
|
+
try {
|
|
159
|
+
concepts = JSON.parse(row.concepts_json);
|
|
160
|
+
} catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(concepts)) continue;
|
|
164
|
+
const selected = new Set<string>();
|
|
165
|
+
for (const entry of concepts) {
|
|
166
|
+
if (
|
|
167
|
+
entry &&
|
|
168
|
+
typeof entry === "object" &&
|
|
169
|
+
(entry as { status?: unknown }).status === "injected" &&
|
|
170
|
+
typeof (entry as { slug?: unknown }).slug === "string"
|
|
171
|
+
) {
|
|
172
|
+
selected.add((entry as { slug: string }).slug);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (selected.size >= 2) turns.push([...selected]);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
log.warn(
|
|
179
|
+
{ err },
|
|
180
|
+
"failed to read router selections for seeding; continuing",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return turns;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build the co-retrieval graph from the v2 router history and persist it into
|
|
188
|
+
* `memory_v3_auto_edges`, weighting each edge by its association strength:
|
|
189
|
+
* `weight = seedWeight × NPMI`. Edges whose weight is non-positive (NPMI ≤ 0 —
|
|
190
|
+
* not an association) are skipped. This makes the read threshold a real
|
|
191
|
+
* minimum-association cutoff rather than all-or-nothing.
|
|
192
|
+
*
|
|
193
|
+
* Idempotent: each edge is upserted by OVERWRITING its weight to the current
|
|
194
|
+
* NPMI-derived value (the seed baseline), so re-running is stable. Tradeoff: the
|
|
195
|
+
* overwrite resets any reinforcement the edge-learning job accrued between seeds.
|
|
196
|
+
* That is acceptable while the seed is the dominant signal and organic
|
|
197
|
+
* co-activation reinforcement is minimal; revisit if/when live reinforcement
|
|
198
|
+
* becomes load-bearing.
|
|
199
|
+
*/
|
|
200
|
+
export function seedCoretrievalEdges(
|
|
201
|
+
database: DrizzleDb,
|
|
202
|
+
options: SeedCoretrievalOptions = DEFAULT_SEED_OPTIONS,
|
|
203
|
+
): SeedCoretrievalResult {
|
|
204
|
+
const turns = readRouterSelections(database);
|
|
205
|
+
const graph = buildCoretrievalGraph(turns, options);
|
|
206
|
+
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
let edgesWritten = 0;
|
|
209
|
+
try {
|
|
210
|
+
const raw = getSqliteFrom(database);
|
|
211
|
+
const upsert = raw.prepare(
|
|
212
|
+
`INSERT INTO memory_v3_auto_edges
|
|
213
|
+
(source_slug, target_slug, weight, last_reinforced_at)
|
|
214
|
+
VALUES (?, ?, ?, ?)
|
|
215
|
+
ON CONFLICT(source_slug, target_slug) DO UPDATE SET
|
|
216
|
+
weight = excluded.weight,
|
|
217
|
+
last_reinforced_at = excluded.last_reinforced_at`,
|
|
218
|
+
);
|
|
219
|
+
const apply = raw.transaction(() => {
|
|
220
|
+
for (const [source, edges] of graph) {
|
|
221
|
+
for (const edge of edges) {
|
|
222
|
+
const weight = options.seedWeight * edge.score;
|
|
223
|
+
if (weight <= 0) continue;
|
|
224
|
+
upsert.run(source, edge.target, weight, now);
|
|
225
|
+
edgesWritten += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
apply();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
log.warn({ err }, "failed to persist seeded co-retrieval edges");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
turnsScanned: turns.length,
|
|
236
|
+
nodes: graph.size,
|
|
237
|
+
edgesWritten,
|
|
238
|
+
avgDegree: graph.size > 0 ? edgesWritten / graph.size : 0,
|
|
239
|
+
};
|
|
240
|
+
}
|
package/src/memory/v3/edges.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* effective out-neighborhood of a node is `curated[node] ∪ extraAdjacency[node]`.
|
|
16
16
|
*
|
|
17
17
|
* The result is the union of every seed's reachable neighborhood (`pulled`,
|
|
18
|
-
* with
|
|
19
|
-
*
|
|
18
|
+
* with the full seed set excluded — a seed reachable from another seed is still
|
|
19
|
+
* a seed, not a neighbor) plus a per-seed `EdgeExpansion[]` trace so a harness
|
|
20
|
+
* can attribute each pulled slug to the seed it came from.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
import { getEdgeIndex, getReachable } from "../v2/edge-index.js";
|
|
@@ -61,10 +62,11 @@ const MAX_SEEDS_EXPANDED = 150;
|
|
|
61
62
|
const MAX_PULLS_PER_SEED = 32;
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
-
* bound: it caps the lane's contribution to the gate's pool
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Default ceiling on the size of the unioned `pulled` set. This is the
|
|
66
|
+
* load-bearing bound: it caps the lane's contribution to the gate's pool no
|
|
67
|
+
* matter how many seeds or how dense the graph. Once reached, no further seeds
|
|
68
|
+
* are expanded. Overridable per call via {@link ExpandEdgesArgs.maxTotalPulls}
|
|
69
|
+
* (the loop wires it to `memory.v3.edges.maxPulls`).
|
|
68
70
|
*/
|
|
69
71
|
const MAX_TOTAL_PULLS = 400;
|
|
70
72
|
|
|
@@ -119,6 +121,12 @@ export interface ExpandEdgesArgs {
|
|
|
119
121
|
* above-threshold edges before passing them in.
|
|
120
122
|
*/
|
|
121
123
|
extraAdjacency?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
124
|
+
/**
|
|
125
|
+
* Hard ceiling on the unioned `pulled` set — the lane's contribution to the
|
|
126
|
+
* gate pool. Defaults to {@link MAX_TOTAL_PULLS} when omitted or not a finite
|
|
127
|
+
* number ≥ 0.
|
|
128
|
+
*/
|
|
129
|
+
maxTotalPulls?: number;
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
export interface ExpandEdgesResult {
|
|
@@ -171,10 +179,11 @@ function reachableMerged(
|
|
|
171
179
|
* Expand a set of confident seed slugs to their 1–2 hop curated neighborhood.
|
|
172
180
|
*
|
|
173
181
|
* Each expanded seed produces one `EdgeExpansion { from, pulled }` entry (sorted
|
|
174
|
-
* slugs for deterministic output);
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* seed
|
|
182
|
+
* slugs for deterministic output); no seed ever appears in `pulled` — not its
|
|
183
|
+
* own entry and not another seed's, even when one seed is a neighbor of another
|
|
184
|
+
* (the seeds-excluded contract). The top-level `pulled` set is the union across
|
|
185
|
+
* all expanded seeds — a slug pulled by more than one seed appears once there
|
|
186
|
+
* but in each contributing seed's expansion.
|
|
178
187
|
*
|
|
179
188
|
* Bounded by {@link MAX_SEEDS_EXPANDED}, {@link MAX_PULLS_PER_SEED}, and
|
|
180
189
|
* {@link MAX_TOTAL_PULLS} so a large seed union or a dense graph can't balloon
|
|
@@ -197,6 +206,15 @@ export async function expandEdges(
|
|
|
197
206
|
laneBySlug,
|
|
198
207
|
} = args;
|
|
199
208
|
|
|
209
|
+
// Per-call override of the union ceiling, falling back to the default constant
|
|
210
|
+
// for an omitted or invalid (negative/NaN) value.
|
|
211
|
+
const maxTotal =
|
|
212
|
+
typeof args.maxTotalPulls === "number" &&
|
|
213
|
+
Number.isFinite(args.maxTotalPulls) &&
|
|
214
|
+
args.maxTotalPulls >= 0
|
|
215
|
+
? args.maxTotalPulls
|
|
216
|
+
: MAX_TOTAL_PULLS;
|
|
217
|
+
|
|
200
218
|
const index = await getEdgeIndex(workspaceDir);
|
|
201
219
|
const pulled = new Set<string>();
|
|
202
220
|
const expansions: EdgeExpansion[] = [];
|
|
@@ -213,7 +231,14 @@ export async function expandEdges(
|
|
|
213
231
|
);
|
|
214
232
|
}
|
|
215
233
|
|
|
216
|
-
// De-dupe seeds while preserving (ranked) order for a stable trace.
|
|
234
|
+
// De-dupe seeds while preserving (ranked) order for a stable trace. The full
|
|
235
|
+
// de-duped seed set is also the exclusion set: a seed reachable from another
|
|
236
|
+
// seed is itself a confident hit, not a neighbor, so it must never land in
|
|
237
|
+
// `pulled` (the seeds-excluded contract) — `getReachable` only excludes the
|
|
238
|
+
// walk's own start node, so a B reachable from A would otherwise leak in.
|
|
239
|
+
const seedSet = new Set<string>();
|
|
240
|
+
for (const seed of orderedSeeds) seedSet.add(seed);
|
|
241
|
+
|
|
217
242
|
const seenSeeds = new Set<string>();
|
|
218
243
|
|
|
219
244
|
for (const seed of orderedSeeds) {
|
|
@@ -223,7 +248,7 @@ export async function expandEdges(
|
|
|
223
248
|
// Bound the number of seeds expanded, and stop once the union is full —
|
|
224
249
|
// the remaining seeds would only inflate the gate's pool. Checked before
|
|
225
250
|
// doing any per-seed work so an oversized seed set is cheap to truncate.
|
|
226
|
-
if (seenSeeds.size > MAX_SEEDS_EXPANDED || pulled.size >=
|
|
251
|
+
if (seenSeeds.size > MAX_SEEDS_EXPANDED || pulled.size >= maxTotal) {
|
|
227
252
|
break;
|
|
228
253
|
}
|
|
229
254
|
|
|
@@ -231,18 +256,30 @@ export async function expandEdges(
|
|
|
231
256
|
? reachableMerged(index.outgoing, extraAdjacency, seed, hops)
|
|
232
257
|
: getReachable(index, seed, hops, "out");
|
|
233
258
|
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// already pulled
|
|
239
|
-
//
|
|
240
|
-
|
|
259
|
+
// Drop any other seed from this seed's neighborhood (seeds-excluded
|
|
260
|
+
// contract) and split the rest into slugs already in the union vs. fresh
|
|
261
|
+
// ones. The per-seed fan-out cap and the union's remaining headroom apply
|
|
262
|
+
// only to fresh slugs, so a slot is never spent on a duplicate an earlier
|
|
263
|
+
// seed already pulled — at the cap that would silently drop a unique
|
|
264
|
+
// neighbor. Sorting first keeps truncation deterministic. The trace lists
|
|
265
|
+
// every reached slug that made it into the bounded union (fresh admissions
|
|
266
|
+
// plus the duplicates this seed also reaches), so it stays a faithful
|
|
267
|
+
// attribution while the budget is reserved for new recall.
|
|
268
|
+
const sorted = [...reachable].sort();
|
|
269
|
+
const alreadyPulled = sorted.filter(
|
|
270
|
+
(slug) => !seedSet.has(slug) && pulled.has(slug),
|
|
271
|
+
);
|
|
272
|
+
const fresh = sorted.filter(
|
|
273
|
+
(slug) => !seedSet.has(slug) && !pulled.has(slug),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const remaining = maxTotal - pulled.size;
|
|
241
277
|
const perSeedCap = Math.min(MAX_PULLS_PER_SEED, remaining);
|
|
242
|
-
const
|
|
278
|
+
const admitted = fresh.slice(0, perSeedCap);
|
|
279
|
+
for (const slug of admitted) pulled.add(slug);
|
|
243
280
|
|
|
281
|
+
const seedPulled = [...alreadyPulled, ...admitted].sort();
|
|
244
282
|
expansions.push({ from: seed, pulled: seedPulled });
|
|
245
|
-
for (const slug of seedPulled) pulled.add(slug);
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
return { pulled, expansions };
|
package/src/memory/v3/filter.ts
CHANGED
|
@@ -12,20 +12,19 @@
|
|
|
12
12
|
* corpus). Hot pages and near-exact sparse hits arrive via the scouts'
|
|
13
13
|
* `sticky` / `bypass` sets and are **never judged**: a literal keyword hit or a
|
|
14
14
|
* page the user has been touching is a strong enough signal that we shouldn't
|
|
15
|
-
* make it earn its place through a fallible cheap judgment
|
|
16
|
-
*
|
|
15
|
+
* make it earn its place through a fallible cheap judgment, and the downstream
|
|
16
|
+
* gate force-injects every sticky slug regardless — judging it could not change
|
|
17
|
+
* its fate. The `bypass` subset is additionally unioned straight into `kept`.
|
|
17
18
|
*
|
|
18
19
|
* Fail-open. If no provider is configured or the call errors / returns an
|
|
19
|
-
* unusable response, the filter keeps *all* dense candidates and surfaces
|
|
20
|
-
* `failureReason` so the loop can record that the filter was bypassed.
|
|
21
|
-
* candidates on a model outage would silently starve retrieval; keeping
|
|
22
|
-
* the safe degradation (the downstream gate still narrows the slate).
|
|
20
|
+
* unusable response, the filter keeps *all* judged dense candidates and surfaces
|
|
21
|
+
* a `failureReason` so the loop can record that the filter was bypassed.
|
|
22
|
+
* Dropping candidates on a model outage would silently starve retrieval; keeping
|
|
23
|
+
* them is the safe degradation (the downstream gate still narrows the slate).
|
|
23
24
|
*
|
|
24
|
-
* No LLM call when there is nothing to judge.
|
|
25
|
-
* to `kept` = the bypass-relevant slugs (no judged
|
|
26
|
-
* round-trip.
|
|
27
|
-
*
|
|
28
|
-
* This module is currently unwired — a later PR composes it into the loop.
|
|
25
|
+
* No LLM call when there is nothing to judge. A dense set fully covered by
|
|
26
|
+
* sticky short-circuits to `kept` = the bypass-relevant slugs (no judged
|
|
27
|
+
* additions), with no provider round-trip.
|
|
29
28
|
*/
|
|
30
29
|
|
|
31
30
|
import { z } from "zod";
|
|
@@ -58,10 +57,12 @@ const FILTER_TOOL_NAME = "filter_dense_hits";
|
|
|
58
57
|
* Arguments to one filter invocation.
|
|
59
58
|
*
|
|
60
59
|
* `dense` is the bounded dense scout result; only its slugs that are *not*
|
|
61
|
-
* already in `
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* already in `sticky` are judged. `sticky` is the keep-in-the-running set (hot +
|
|
61
|
+
* near-exact sparse) the downstream gate force-injects regardless of this
|
|
62
|
+
* filter, so judging a sticky page wastes an LLM call that can never change its
|
|
63
|
+
* fate. `bypass` is the subset of sticky strong enough to skip judgment that the
|
|
64
|
+
* filter also unions straight into `kept`. Sticky slugs that also appear in the
|
|
65
|
+
* dense lane are excluded from the judged set and never sent to the model.
|
|
65
66
|
*/
|
|
66
67
|
export interface FilterDenseHitsArgs {
|
|
67
68
|
input: RetrievalInput;
|
|
@@ -163,19 +164,23 @@ function buildResult(
|
|
|
163
164
|
* Run the fast dense-hit filter for one pass.
|
|
164
165
|
*
|
|
165
166
|
* Makes at most one forced-tool LLM call over the *judged* set (dense slugs not
|
|
166
|
-
* already in `
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* already in `sticky`). Sticky slugs are force-selected by the downstream gate
|
|
168
|
+
* regardless of this filter, so they are excluded from judgment; bypass slugs
|
|
169
|
+
* are additionally kept unconditionally here. On an empty judged set no call is
|
|
170
|
+
* made. Any failure (no provider, provider throw, missing tool_use, schema
|
|
171
|
+
* mismatch) fails open: every judged dense candidate is kept and a
|
|
169
172
|
* `failureReason` is returned.
|
|
170
173
|
*/
|
|
171
174
|
export async function filterDenseHits(
|
|
172
175
|
args: FilterDenseHitsArgs,
|
|
173
176
|
): Promise<FilterDenseHitsResult> {
|
|
174
|
-
const { input, dense, bypass } = args;
|
|
177
|
+
const { input, dense, sticky, bypass } = args;
|
|
175
178
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
+
// Sticky slugs (hot + near-exact sparse) are force-selected by the gate
|
|
180
|
+
// regardless of this filter, so judging them wastes an LLM call that can't
|
|
181
|
+
// change their fate. Exclude the full sticky set (a superset of bypass) from
|
|
182
|
+
// the judged set; only the remaining dense near-neighbors are judged.
|
|
183
|
+
const judged = dense.slugs.filter((slug) => !sticky.has(slug));
|
|
179
184
|
|
|
180
185
|
// Nothing to judge → no LLM call. Kept is just the bypass-relevant slugs.
|
|
181
186
|
if (judged.length === 0) {
|
package/src/memory/v3/gate.ts
CHANGED
|
@@ -79,9 +79,9 @@ export interface RunGateArgs {
|
|
|
79
79
|
* Per-candidate one-line summaries, keyed by slug. When present, candidates
|
|
80
80
|
* are rendered to the model as `slug — summary` so the gate can judge
|
|
81
81
|
* relevance on page content rather than the slug alone. Missing entries fall
|
|
82
|
-
* back to the bare slug; the
|
|
83
|
-
*
|
|
84
|
-
* is set.
|
|
82
|
+
* back to the bare slug; the model answers with the candidate's `[N]` number
|
|
83
|
+
* (`selected_ids`) regardless, so summaries never change the answer format. The
|
|
84
|
+
* loop passes this only when `memory.v3.gateCandidateSummaries` is set.
|
|
85
85
|
*/
|
|
86
86
|
summaryBySlug?: ReadonlyMap<string, string>;
|
|
87
87
|
/** Optional debug sink — emits one record for the gate's LLM call. */
|
|
@@ -102,17 +102,20 @@ export interface RunGateResult {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Build the forced tool definition. `
|
|
106
|
-
*
|
|
107
|
-
* generated follow-up queries on "more" (ignored on
|
|
108
|
-
*
|
|
105
|
+
* Build the forced tool definition. `selected_ids` is the list of bracketed
|
|
106
|
+
* `[N]` candidate numbers to keep; `decision` is the ready/more verdict;
|
|
107
|
+
* `questions` carries the generated follow-up queries on "more" (ignored on
|
|
108
|
+
* "ready"). The schema is candidate-independent — no per-turn enum — so it stays
|
|
109
|
+
* byte-identical across turns (cache-friendly) and the model answers with the
|
|
110
|
+
* line numbers rather than reproducing exact slug strings, mirroring the
|
|
111
|
+
* integer-id output of v2's `select_pages_to_inject`.
|
|
109
112
|
*/
|
|
110
|
-
function buildGateTool(
|
|
113
|
+
function buildGateTool(): ToolDefinition {
|
|
111
114
|
return {
|
|
112
115
|
name: GATE_TOOL_NAME,
|
|
113
116
|
description:
|
|
114
117
|
"Decide whether the accumulated candidate pages are sufficient to answer " +
|
|
115
|
-
"the next turn. Return decision='ready' with the final
|
|
118
|
+
"the next turn. Return decision='ready' with the final selection " +
|
|
116
119
|
"when the candidates cover the turn; return decision='more' with one or " +
|
|
117
120
|
"more generated follow-up questions (NOT the original message) to seed " +
|
|
118
121
|
"another retrieval pass when coverage is incomplete.",
|
|
@@ -120,16 +123,14 @@ function buildGateTool(candidateSlugs: readonly string[]): ToolDefinition {
|
|
|
120
123
|
type: "object",
|
|
121
124
|
properties: {
|
|
122
125
|
decision: { type: "string", enum: ["ready", "more"] },
|
|
123
|
-
|
|
126
|
+
selected_ids: {
|
|
124
127
|
type: "array",
|
|
125
|
-
items: { type: "
|
|
128
|
+
items: { type: "integer" },
|
|
126
129
|
description:
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"breadth request, include every candidate that plausibly applies rather " +
|
|
132
|
-
"than trimming to the most prominent few.",
|
|
130
|
+
"The bracketed `[N]` numbers of the candidate pages to keep. Prefer " +
|
|
131
|
+
"keeping a plausibly-relevant page over dropping it; for a list / " +
|
|
132
|
+
"'all of X' / breadth request, include every candidate that plausibly " +
|
|
133
|
+
"applies rather than trimming to the most prominent few.",
|
|
133
134
|
},
|
|
134
135
|
questions: {
|
|
135
136
|
type: "array",
|
|
@@ -144,32 +145,33 @@ function buildGateTool(candidateSlugs: readonly string[]): ToolDefinition {
|
|
|
144
145
|
"candidates were kept or dropped.",
|
|
145
146
|
},
|
|
146
147
|
},
|
|
147
|
-
required: ["decision"],
|
|
148
|
+
required: ["decision", "selected_ids"],
|
|
148
149
|
},
|
|
149
150
|
};
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
const GateToolResultSchema = z.object({
|
|
153
154
|
decision: z.enum(["ready", "more"]),
|
|
154
|
-
|
|
155
|
+
selected_ids: z.array(z.coerce.number()).optional(),
|
|
155
156
|
questions: z.array(z.string()).optional(),
|
|
156
157
|
reasoning: z.string().optional(),
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
* Render the candidate list for the prompt.
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
161
|
+
* Render the candidate list for the prompt. Each line is `[N] slug — summary`
|
|
162
|
+
* (or `[N] slug` without a summary), numbered from 1 in candidate order. The
|
|
163
|
+
* model answers with the `[N]` numbers via `selected_ids`, which the caller maps
|
|
164
|
+
* back to slugs by index — so the tool schema stays candidate-independent.
|
|
164
165
|
*/
|
|
165
166
|
function renderCandidateLines(
|
|
166
167
|
slugs: readonly string[],
|
|
167
168
|
summaryBySlug: ReadonlyMap<string, string> | undefined,
|
|
168
169
|
): string {
|
|
169
170
|
return slugs
|
|
170
|
-
.map((slug) => {
|
|
171
|
+
.map((slug, i) => {
|
|
171
172
|
const summary = summaryBySlug?.get(slug);
|
|
172
|
-
|
|
173
|
+
const body = summary ? `${slug} — ${summary}` : slug;
|
|
174
|
+
return `[${i + 1}] ${body}`;
|
|
173
175
|
})
|
|
174
176
|
.join("\n");
|
|
175
177
|
}
|
|
@@ -267,7 +269,7 @@ export async function runGate(args: RunGateArgs): Promise<RunGateResult> {
|
|
|
267
269
|
],
|
|
268
270
|
};
|
|
269
271
|
|
|
270
|
-
const gateTool = buildGateTool(
|
|
272
|
+
const gateTool = buildGateTool();
|
|
271
273
|
|
|
272
274
|
const startedAt = Date.now();
|
|
273
275
|
let response;
|
|
@@ -310,25 +312,38 @@ export async function runGate(args: RunGateArgs): Promise<RunGateResult> {
|
|
|
310
312
|
return failSafe(candidates, sticky);
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
// Recall-safe fallback: an *omitted* `selected_ids` means the model gave no
|
|
316
|
+
// instruction, so keep everything that was surfaced — dropping all non-sticky
|
|
317
|
+
// context on a silent omission is the worse failure. An *explicit* `[]` is the
|
|
318
|
+
// model genuinely choosing nothing and is honored as-is. Ids are 1-based
|
|
319
|
+
// indices into the rendered candidate list; out-of-range ids are dropped.
|
|
320
|
+
const modelSlugs =
|
|
321
|
+
parsed.data.selected_ids === undefined
|
|
322
|
+
? [...candidates]
|
|
323
|
+
: parsed.data.selected_ids
|
|
324
|
+
.map((id) => candidateSlugs[Math.trunc(id) - 1])
|
|
325
|
+
.filter((slug): slug is string => slug !== undefined);
|
|
326
|
+
const selectedSlugs = orderSelection(modelSlugs, candidates, sticky);
|
|
327
|
+
|
|
328
|
+
const reasoning = parsed.data.reasoning?.trim() || undefined;
|
|
318
329
|
|
|
319
330
|
if (parsed.data.decision === "more") {
|
|
320
331
|
const questions = (parsed.data.questions ?? []).filter(
|
|
321
332
|
(q) => q.trim().length > 0,
|
|
322
333
|
);
|
|
323
|
-
const decision: GateDecision =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
const decision: GateDecision = {
|
|
335
|
+
decision: "more",
|
|
336
|
+
...(questions.length > 0 ? { questions } : {}),
|
|
337
|
+
...(reasoning ? { reasoning } : {}),
|
|
338
|
+
};
|
|
327
339
|
return { decision, selectedSlugs };
|
|
328
340
|
}
|
|
329
341
|
|
|
330
342
|
// brief generation lands at cutover (P5) — shadow mode injects v2, so this
|
|
331
343
|
// gate produces only the selection + decision. Do NOT synthesize a voice
|
|
332
344
|
// brief here.
|
|
333
|
-
return {
|
|
345
|
+
return {
|
|
346
|
+
decision: { decision: "ready", ...(reasoning ? { reasoning } : {}) },
|
|
347
|
+
selectedSlugs,
|
|
348
|
+
};
|
|
334
349
|
}
|