@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
|
@@ -35,6 +35,16 @@ import type { TreeNode } from "./types.js";
|
|
|
35
35
|
/** Trailer label introducing a node's own routing hints. */
|
|
36
36
|
const ROUTING_HINTS_LABEL = "Routing hints:";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Flatten a summary to a single line. The index is parsed by the descender as
|
|
40
|
+
* one child per line, so any embedded newline (or other whitespace run) in a
|
|
41
|
+
* summary would corrupt that format — collapse every whitespace run to a single
|
|
42
|
+
* space and trim the ends.
|
|
43
|
+
*/
|
|
44
|
+
function collapseToSingleLine(text: string): string {
|
|
45
|
+
return text.replace(/\s+/g, " ").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
/**
|
|
39
49
|
* Resolve a node's display summary: its frontmatter `summary` if non-empty,
|
|
40
50
|
* otherwise the first non-empty line of its body, otherwise the empty string.
|
|
@@ -54,9 +64,11 @@ function nodeSummary(node: TreeNode): string {
|
|
|
54
64
|
* Render one child ref into its index line, or `null` when the ref's target is
|
|
55
65
|
* absent from the supplied indices (validation owns reporting those).
|
|
56
66
|
*
|
|
57
|
-
* A resolvable
|
|
58
|
-
* with a trailing summary when one exists.
|
|
59
|
-
*
|
|
67
|
+
* A resolvable child always yields a line — its header (`[node:<id>]` /
|
|
68
|
+
* `[page:<slug>]`) with a trailing summary when one exists. Each summary is
|
|
69
|
+
* collapsed to a single line so an embedded newline can't break the one-child-
|
|
70
|
+
* per-line format the descender parses; the v2 page index already truncates
|
|
71
|
+
* `page:` summaries.
|
|
60
72
|
*/
|
|
61
73
|
function renderChild(
|
|
62
74
|
kind: "page" | "node",
|
|
@@ -67,12 +79,13 @@ function renderChild(
|
|
|
67
79
|
if (kind === "node") {
|
|
68
80
|
const child = tree.nodes.get(ref);
|
|
69
81
|
if (!child) return null;
|
|
70
|
-
const summary = nodeSummary(child);
|
|
82
|
+
const summary = collapseToSingleLine(nodeSummary(child));
|
|
71
83
|
return summary ? `[node:${ref}] ${summary}` : `[node:${ref}]`;
|
|
72
84
|
}
|
|
73
85
|
const entry = pages.bySlug.get(ref);
|
|
74
86
|
if (!entry) return null;
|
|
75
|
-
|
|
87
|
+
const summary = collapseToSingleLine(entry.summary);
|
|
88
|
+
return summary ? `[page:${ref}] ${summary}` : `[page:${ref}]`;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
/**
|
package/src/memory/v3/loop.ts
CHANGED
|
@@ -36,12 +36,14 @@
|
|
|
36
36
|
* {@link runScouts}. Toggling a lane off removes its contribution from the
|
|
37
37
|
* candidate set so the offline harness can measure each lane's marginal recall.
|
|
38
38
|
*
|
|
39
|
-
* Cross-pass accumulation.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Cross-pass accumulation. The `candidates` pool is unioned across every pass
|
|
40
|
+
* and the gate judges that cumulative pool, so a multi-pass `more` never drops
|
|
41
|
+
* the non-sticky hits earlier passes surfaced. Each slug is tagged with the
|
|
42
|
+
* most trusted lane that surfaced it (`sourceBySlug`). The full
|
|
43
|
+
* {@link DescentTrace}
|
|
44
|
+
* carries one {@link DescentPass} per pass (scouts / treeLevels /
|
|
45
|
+
* edgeExpansions / gate), and {@link RetrievalCost} (wall-clock `ms`, the one
|
|
46
|
+
* dimension observable at this composition layer) accumulates across every pass.
|
|
45
47
|
*/
|
|
46
48
|
|
|
47
49
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -57,6 +59,7 @@ import type {
|
|
|
57
59
|
GateDecision,
|
|
58
60
|
} from "../v2/harness/trace.js";
|
|
59
61
|
import { getPageIndex } from "../v2/page-index.js";
|
|
62
|
+
import { aboveThreshold } from "./auto-edges.js";
|
|
60
63
|
import {
|
|
61
64
|
type CoactivationRow,
|
|
62
65
|
recordCoactivations,
|
|
@@ -111,8 +114,22 @@ export async function runRetrievalLoop(
|
|
|
111
114
|
const passCap = Math.max(1, v3.passCap);
|
|
112
115
|
const lanes = v3.lanes;
|
|
113
116
|
|
|
117
|
+
// Learned co-retrieval adjacency (memory_v3_auto_edges), read once and merged
|
|
118
|
+
// into the edge lane's curated graph when the threshold is set. At threshold 0
|
|
119
|
+
// (the default) this is undefined and edge behavior is identical to before.
|
|
120
|
+
const learnedAdjacencyThreshold = v3.edges?.learnedAdjacencyThreshold ?? 0;
|
|
121
|
+
const learnedAdjacency =
|
|
122
|
+
learnedAdjacencyThreshold > 0
|
|
123
|
+
? aboveThreshold(deps.db, learnedAdjacencyThreshold)
|
|
124
|
+
: undefined;
|
|
125
|
+
|
|
114
126
|
// Cross-pass accumulators.
|
|
115
127
|
const sourceBySlug = new Map<string, LaneSource>();
|
|
128
|
+
// Candidate pool unioned across every pass. Each pass adds its own surfaced
|
|
129
|
+
// slugs (hot/sparse, dense-filter survivors, tree, edge) and the gate judges
|
|
130
|
+
// the cumulative pool, so a multi-pass `more` never discards earlier passes'
|
|
131
|
+
// non-sticky hits.
|
|
132
|
+
const candidates = new Set<string>();
|
|
116
133
|
// The first pass each slug entered the candidate set. Drives co-activation
|
|
117
134
|
// emission below — pass-1 hits (gap source) vs. later-surfaced pages (target).
|
|
118
135
|
const firstPassBySlug = new Map<string, number>();
|
|
@@ -148,18 +165,19 @@ export async function runRetrievalLoop(
|
|
|
148
165
|
const scoutResult = await runScouts(passInput, { db: deps.db });
|
|
149
166
|
for (const slug of scoutResult.sticky) sticky.add(slug);
|
|
150
167
|
|
|
151
|
-
// Tag hot + sparse scout hits with their lane (
|
|
152
|
-
// slugs are tagged only if they survive the filter
|
|
153
|
-
// near-neighbor never enters the candidate set, so
|
|
168
|
+
// Tag hot + sparse scout hits with their lane (most trusted lane wins —
|
|
169
|
+
// see tagSlug). Dense slugs are tagged only if they survive the filter
|
|
170
|
+
// below — a dropped dense near-neighbor never enters the candidate set, so
|
|
171
|
+
// it earns no source tag.
|
|
154
172
|
for (const scout of scoutResult.scouts) {
|
|
155
173
|
if (scout.lane === "dense") continue;
|
|
156
174
|
for (const slug of scout.slugs) tagSlug(sourceBySlug, slug, scout.lane);
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
// 2. Dense filter — judges only the dense lane (hot/sparse bypass it).
|
|
160
|
-
// surviving dense slugs
|
|
177
|
+
// 2. Dense filter — judges only the dense lane (hot/sparse bypass it). Only
|
|
178
|
+
// the surviving dense slugs enter the candidate pool; a dropped dense
|
|
179
|
+
// near-neighbor never joins it (and so never reaches the gate).
|
|
161
180
|
const denseScout = scoutResult.scouts.find((s) => s.lane === "dense");
|
|
162
|
-
const candidates = new Set<string>();
|
|
163
181
|
|
|
164
182
|
// Hot + sparse lane hits enter the candidate set directly.
|
|
165
183
|
for (const scout of scoutResult.scouts) {
|
|
@@ -218,6 +236,11 @@ export async function runRetrievalLoop(
|
|
|
218
236
|
// hot) so the seed cap spends its budget on query-relevant seeds, not
|
|
219
237
|
// recency. `sourceBySlug` holds each candidate's first-seen lane.
|
|
220
238
|
laneBySlug: sourceBySlug,
|
|
239
|
+
// Merge the learned co-retrieval graph with the curated edges when
|
|
240
|
+
// enabled (undefined = curated-only, the default).
|
|
241
|
+
...(learnedAdjacency ? { extraAdjacency: learnedAdjacency } : {}),
|
|
242
|
+
// Cap the lane's contribution to the gate pool (default 400).
|
|
243
|
+
maxTotalPulls: v3.edges?.maxPulls,
|
|
221
244
|
});
|
|
222
245
|
edgeExpansions = expansion.expansions;
|
|
223
246
|
for (const slug of expansion.pulled) {
|
|
@@ -354,17 +377,42 @@ function emitCoactivations(args: {
|
|
|
354
377
|
}
|
|
355
378
|
|
|
356
379
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
380
|
+
* Lane-trust order for `sourceBySlug` provenance (lower = more trusted). A slug
|
|
381
|
+
* surfaced by several lanes is tagged with the most trusted one. Mirrors
|
|
382
|
+
* `SEED_LANE_RANK` in {@link expandEdges}'s module, the downstream consumer that
|
|
383
|
+
* ranks seeds by this tag before the candidate cap: LLM-vetted tree/dense seeds
|
|
384
|
+
* rank above lexical sparse, recency-only hot, and associative edge pulls.
|
|
385
|
+
* Keeping the two orders aligned ensures the upgrade picks the lane the cap
|
|
386
|
+
* actually trusts. Any lane absent here (or an edge pull) ranks last.
|
|
387
|
+
*/
|
|
388
|
+
const LANE_TRUST_RANK: Readonly<Record<LaneSource, number>> = {
|
|
389
|
+
tree: 0,
|
|
390
|
+
dense: 1,
|
|
391
|
+
sparse: 2,
|
|
392
|
+
hot: 3,
|
|
393
|
+
edge: 4,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Tag `slug`'s provenance with `lane`, upgrading to the most trusted lane that
|
|
398
|
+
* surfaces it (see {@link LANE_TRUST_RANK}). A slug first seen via a low-trust
|
|
399
|
+
* lane (e.g. `edge`) is relabeled when a higher-trust lane (e.g. `dense`) also
|
|
400
|
+
* surfaces it, so the downstream seed cap ranks it by its strongest signal
|
|
401
|
+
* rather than a stale first-seen lane. Pass provenance (`firstPassBySlug`) is
|
|
402
|
+
* tracked separately and keeps earliest-pass semantics — only the lane upgrades.
|
|
361
403
|
*/
|
|
362
404
|
function tagSlug(
|
|
363
405
|
sourceBySlug: Map<string, LaneSource>,
|
|
364
406
|
slug: string,
|
|
365
407
|
lane: LaneSource,
|
|
366
408
|
): void {
|
|
367
|
-
|
|
409
|
+
const current = sourceBySlug.get(slug);
|
|
410
|
+
if (
|
|
411
|
+
current === undefined ||
|
|
412
|
+
LANE_TRUST_RANK[lane] < LANE_TRUST_RANK[current]
|
|
413
|
+
) {
|
|
414
|
+
sourceBySlug.set(slug, lane);
|
|
415
|
+
}
|
|
368
416
|
}
|
|
369
417
|
|
|
370
418
|
/**
|
package/src/memory/v3/scouts.ts
CHANGED
|
@@ -361,11 +361,22 @@ function applyQuotaAndMmr(
|
|
|
361
361
|
function mmrReorder(pool: readonly ScoredSlug[], lambda: number): ScoredSlug[] {
|
|
362
362
|
if (pool.length <= 1) return [...pool];
|
|
363
363
|
|
|
364
|
-
// Normalize relevance to [0, 1]
|
|
365
|
-
//
|
|
364
|
+
// Normalize relevance to [0, 1] so it shares a scale with the redundancy term
|
|
365
|
+
// (also [0, 1]). The pool is score-descending, so `pool[0]` is the max and the
|
|
366
|
+
// last entry is the min. When the max is positive we divide by it — the
|
|
367
|
+
// healthy case, kept exactly as-is. When every cosine is <= 0 (all candidates
|
|
368
|
+
// weakly/negatively similar) dividing by the max would collapse relevance to a
|
|
369
|
+
// single value and degrade ranking to diversity-only; instead normalize from
|
|
370
|
+
// the observed min..max range so the relevance ordering is preserved. A
|
|
371
|
+
// zero-width range (all scores equal) has no gradient to preserve, so it
|
|
372
|
+
// collapses to pure diversity.
|
|
366
373
|
const maxScore = pool[0].score;
|
|
367
|
-
const
|
|
368
|
-
|
|
374
|
+
const minScore = pool[pool.length - 1].score;
|
|
375
|
+
const range = maxScore - minScore;
|
|
376
|
+
const relevance = (hit: ScoredSlug): number => {
|
|
377
|
+
if (maxScore > 0) return hit.score / maxScore;
|
|
378
|
+
return range > 0 ? (hit.score - minScore) / range : 0;
|
|
379
|
+
};
|
|
369
380
|
|
|
370
381
|
const remaining = [...pool];
|
|
371
382
|
const selected: ScoredSlug[] = [];
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory v3 shadow-diff — compare the v3 shadow selection against the live v2
|
|
3
|
+
* router selection, turn-for-turn, from the `memory_v2_activation_logs` table.
|
|
4
|
+
*
|
|
5
|
+
* When v3 runs in shadow mode it logs its per-turn selection as a `v3_shadow`
|
|
6
|
+
* row while the live v2 router logs its own selection as a `router` row on the
|
|
7
|
+
* same turn. This module pairs the two and reports where they agree, what v3
|
|
8
|
+
* surfaced that v2 did not, and what v2 had that v3 dropped — broken down by the
|
|
9
|
+
* v3 provenance lane so a shadow run is analyzable by where its recall comes
|
|
10
|
+
* from.
|
|
11
|
+
*
|
|
12
|
+
* Pairing is by timestamp, NOT by the `turn` column: the shadow middleware logs
|
|
13
|
+
* the orchestrator's per-runtime turn counter while v2 logs the cumulative
|
|
14
|
+
* conversation turn, so the two numbers diverge. The shadow row and its sibling
|
|
15
|
+
* router row are written within the same turn (a second or two apart), so each
|
|
16
|
+
* shadow row is matched to the nearest router row in the same conversation
|
|
17
|
+
* within a tolerance window.
|
|
18
|
+
*
|
|
19
|
+
* The v2 comparand is the router's FRESH per-turn pick (`status: "injected"`),
|
|
20
|
+
* not its full in-context set. v2 accumulates pages across turns (`in_context`
|
|
21
|
+
* carry-over can reach the hundreds on a long conversation) whereas v3 selects
|
|
22
|
+
* fresh each turn; comparing fresh-against-fresh keeps the diff meaningful. The
|
|
23
|
+
* carried-over count is surfaced per turn as context, not folded into the sets.
|
|
24
|
+
*
|
|
25
|
+
* Pure and DB-free: the route handler reads the rows and hands them here.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { MemoryV2ConceptRowRecord } from "../memory-v2-activation-log-store.js";
|
|
29
|
+
|
|
30
|
+
/** An activation-log row reduced to what the diff needs. */
|
|
31
|
+
export interface ShadowDiffLogRow {
|
|
32
|
+
conversationId: string;
|
|
33
|
+
/** Epoch milliseconds. */
|
|
34
|
+
createdAt: number;
|
|
35
|
+
concepts: MemoryV2ConceptRowRecord[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One paired (v2 router ↔ v3 shadow) turn. */
|
|
39
|
+
export interface ShadowDiffTurn {
|
|
40
|
+
conversationId: string;
|
|
41
|
+
/** Epoch ms of the v3 shadow row. */
|
|
42
|
+
shadowAt: number;
|
|
43
|
+
/** Epoch ms of the paired v2 router row. */
|
|
44
|
+
routerAt: number;
|
|
45
|
+
/** `routerAt - shadowAt`; small (within tolerance) by construction. */
|
|
46
|
+
deltaMs: number;
|
|
47
|
+
/** Size of the v2 fresh pick (`status: "injected"`). */
|
|
48
|
+
v2Count: number;
|
|
49
|
+
/** Size of the v3 shadow selection. */
|
|
50
|
+
v3Count: number;
|
|
51
|
+
/** v2 pages carried over from prior turns (`in_context`); annotation only. */
|
|
52
|
+
v2CachedCount: number;
|
|
53
|
+
/** `|overlap| / |v2 ∪ v3|`; 0 when both sets are empty. */
|
|
54
|
+
jaccard: number;
|
|
55
|
+
/** Slugs both systems picked, sorted. */
|
|
56
|
+
overlap: string[];
|
|
57
|
+
/** Slugs v3 surfaced but v2 did not freshly inject, sorted. */
|
|
58
|
+
v3Only: string[];
|
|
59
|
+
/** Slugs v2 freshly injected but v3 missed, sorted. */
|
|
60
|
+
v2Only: string[];
|
|
61
|
+
/** Provenance lane for each v3 slug (overlap + v3-only). */
|
|
62
|
+
laneBySlug: Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A shadow row with no router row inside the tolerance window. */
|
|
66
|
+
export interface UnpairedShadowTurn {
|
|
67
|
+
conversationId: string;
|
|
68
|
+
shadowAt: number;
|
|
69
|
+
v3Count: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A slug with how many paired turns it appeared in. */
|
|
73
|
+
export interface SlugFrequency {
|
|
74
|
+
slug: string;
|
|
75
|
+
count: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ShadowDiffResult {
|
|
79
|
+
/** Pairing tolerance actually used (ms). */
|
|
80
|
+
toleranceMs: number;
|
|
81
|
+
/** Total v3 shadow rows in the read window. */
|
|
82
|
+
shadowRows: number;
|
|
83
|
+
/** Shadow rows that paired to a router row. */
|
|
84
|
+
turnsCompared: number;
|
|
85
|
+
/** Shadow rows that did not pair. */
|
|
86
|
+
unpaired: UnpairedShadowTurn[];
|
|
87
|
+
agg: {
|
|
88
|
+
meanV2: number;
|
|
89
|
+
meanV3: number;
|
|
90
|
+
meanOverlap: number;
|
|
91
|
+
meanJaccard: number;
|
|
92
|
+
totalOverlap: number;
|
|
93
|
+
totalV3Only: number;
|
|
94
|
+
totalV2Only: number;
|
|
95
|
+
/** v3-only slug count by the lane that surfaced it — v3's extra reach. */
|
|
96
|
+
v3OnlyByLane: Record<string, number>;
|
|
97
|
+
/** overlap slug count by the v3 lane that recovered v2's pick. */
|
|
98
|
+
overlapByLane: Record<string, number>;
|
|
99
|
+
/** Most frequently dropped v2 pages (recall-regression watchlist). */
|
|
100
|
+
v2OnlyTop: SlugFrequency[];
|
|
101
|
+
/** Most frequent v3 extras (associative reach beyond v2). */
|
|
102
|
+
v3OnlyTop: SlugFrequency[];
|
|
103
|
+
};
|
|
104
|
+
/** Per-turn detail, newest first, capped at the requested limit. */
|
|
105
|
+
turns: ShadowDiffTurn[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Status on a v2 router row that counts as a fresh per-turn selection. */
|
|
109
|
+
const V2_PICKED_STATUS = "injected";
|
|
110
|
+
/** Status on a v2 router row that means carried-over from a prior turn. */
|
|
111
|
+
const V2_CACHED_STATUS = "in_context";
|
|
112
|
+
/** How many slugs to list in the top-frequency aggregates. */
|
|
113
|
+
const TOP_FREQUENCY_LIMIT = 15;
|
|
114
|
+
|
|
115
|
+
function selectedV2Slugs(concepts: MemoryV2ConceptRowRecord[]): Set<string> {
|
|
116
|
+
const slugs = new Set<string>();
|
|
117
|
+
for (const c of concepts) {
|
|
118
|
+
if (c.status === V2_PICKED_STATUS) slugs.add(c.slug);
|
|
119
|
+
}
|
|
120
|
+
return slugs;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function cachedV2Count(concepts: MemoryV2ConceptRowRecord[]): number {
|
|
124
|
+
let n = 0;
|
|
125
|
+
for (const c of concepts) {
|
|
126
|
+
if (c.status === V2_CACHED_STATUS) n += 1;
|
|
127
|
+
}
|
|
128
|
+
return n;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function v3LaneBySlug(
|
|
132
|
+
concepts: MemoryV2ConceptRowRecord[],
|
|
133
|
+
): Map<string, string> {
|
|
134
|
+
const bySlug = new Map<string, string>();
|
|
135
|
+
for (const c of concepts) {
|
|
136
|
+
bySlug.set(c.slug, c.lane ?? "unknown");
|
|
137
|
+
}
|
|
138
|
+
return bySlug;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Increment a string-keyed tally in place. */
|
|
142
|
+
function bump(tally: Record<string, number>, key: string): void {
|
|
143
|
+
tally[key] = (tally[key] ?? 0) + 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Sort a frequency map into the top-N slugs, ties broken by slug name. */
|
|
147
|
+
function topSlugs(freq: Map<string, number>, limit: number): SlugFrequency[] {
|
|
148
|
+
return [...freq.entries()]
|
|
149
|
+
.map(([slug, count]) => ({ slug, count }))
|
|
150
|
+
.sort((a, b) => b.count - a.count || a.slug.localeCompare(b.slug))
|
|
151
|
+
.slice(0, limit);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Pair each shadow row to the nearest unconsumed router row in the same
|
|
156
|
+
* conversation within `toleranceMs`, then diff the two selections. Pairing is
|
|
157
|
+
* greedy by absolute time delta; since real turns are minutes apart while a
|
|
158
|
+
* shadow/router sibling pair lands a second or two apart, the greedy match is a
|
|
159
|
+
* clean bijection in practice.
|
|
160
|
+
*/
|
|
161
|
+
export function computeShadowDiff(
|
|
162
|
+
shadow: readonly ShadowDiffLogRow[],
|
|
163
|
+
router: readonly ShadowDiffLogRow[],
|
|
164
|
+
opts: { toleranceMs: number; detailLimit: number },
|
|
165
|
+
): ShadowDiffResult {
|
|
166
|
+
const { toleranceMs, detailLimit } = opts;
|
|
167
|
+
|
|
168
|
+
// Index router rows by conversation, time-sorted, with a consumed flag so a
|
|
169
|
+
// router row pairs to at most one shadow row.
|
|
170
|
+
const routerByConv = new Map<
|
|
171
|
+
string,
|
|
172
|
+
Array<{ row: ShadowDiffLogRow; consumed: boolean }>
|
|
173
|
+
>();
|
|
174
|
+
for (const row of router) {
|
|
175
|
+
const bucket = routerByConv.get(row.conversationId) ?? [];
|
|
176
|
+
bucket.push({ row, consumed: false });
|
|
177
|
+
routerByConv.set(row.conversationId, bucket);
|
|
178
|
+
}
|
|
179
|
+
for (const bucket of routerByConv.values()) {
|
|
180
|
+
bucket.sort((a, b) => a.row.createdAt - b.row.createdAt);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sortedShadow = [...shadow].sort((a, b) => a.createdAt - b.createdAt);
|
|
184
|
+
|
|
185
|
+
const turns: ShadowDiffTurn[] = [];
|
|
186
|
+
const unpaired: UnpairedShadowTurn[] = [];
|
|
187
|
+
const v3OnlyByLane: Record<string, number> = {};
|
|
188
|
+
const overlapByLane: Record<string, number> = {};
|
|
189
|
+
const v2OnlyFreq = new Map<string, number>();
|
|
190
|
+
const v3OnlyFreq = new Map<string, number>();
|
|
191
|
+
|
|
192
|
+
for (const sh of sortedShadow) {
|
|
193
|
+
const bucket = routerByConv.get(sh.conversationId);
|
|
194
|
+
let best: { row: ShadowDiffLogRow; consumed: boolean } | undefined;
|
|
195
|
+
let bestDelta = Number.POSITIVE_INFINITY;
|
|
196
|
+
for (const candidate of bucket ?? []) {
|
|
197
|
+
if (candidate.consumed) continue;
|
|
198
|
+
const delta = Math.abs(candidate.row.createdAt - sh.createdAt);
|
|
199
|
+
if (delta < bestDelta) {
|
|
200
|
+
bestDelta = delta;
|
|
201
|
+
best = candidate;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!best || bestDelta > toleranceMs) {
|
|
206
|
+
unpaired.push({
|
|
207
|
+
conversationId: sh.conversationId,
|
|
208
|
+
shadowAt: sh.createdAt,
|
|
209
|
+
v3Count: new Set(sh.concepts.map((c) => c.slug)).size,
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
best.consumed = true;
|
|
214
|
+
|
|
215
|
+
const v2Set = selectedV2Slugs(best.row.concepts);
|
|
216
|
+
const laneBySlug = v3LaneBySlug(sh.concepts);
|
|
217
|
+
const v3Set = new Set(laneBySlug.keys());
|
|
218
|
+
|
|
219
|
+
const overlap: string[] = [];
|
|
220
|
+
const v3Only: string[] = [];
|
|
221
|
+
for (const slug of v3Set) {
|
|
222
|
+
if (v2Set.has(slug)) {
|
|
223
|
+
overlap.push(slug);
|
|
224
|
+
bump(overlapByLane, laneBySlug.get(slug)!);
|
|
225
|
+
} else {
|
|
226
|
+
v3Only.push(slug);
|
|
227
|
+
bump(v3OnlyByLane, laneBySlug.get(slug)!);
|
|
228
|
+
v3OnlyFreq.set(slug, (v3OnlyFreq.get(slug) ?? 0) + 1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const v2Only: string[] = [];
|
|
232
|
+
for (const slug of v2Set) {
|
|
233
|
+
if (!v3Set.has(slug)) {
|
|
234
|
+
v2Only.push(slug);
|
|
235
|
+
v2OnlyFreq.set(slug, (v2OnlyFreq.get(slug) ?? 0) + 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const unionSize = new Set([...v2Set, ...v3Set]).size;
|
|
240
|
+
turns.push({
|
|
241
|
+
conversationId: sh.conversationId,
|
|
242
|
+
shadowAt: sh.createdAt,
|
|
243
|
+
routerAt: best.row.createdAt,
|
|
244
|
+
deltaMs: best.row.createdAt - sh.createdAt,
|
|
245
|
+
v2Count: v2Set.size,
|
|
246
|
+
v3Count: v3Set.size,
|
|
247
|
+
v2CachedCount: cachedV2Count(best.row.concepts),
|
|
248
|
+
jaccard: unionSize === 0 ? 0 : overlap.length / unionSize,
|
|
249
|
+
overlap: overlap.sort(),
|
|
250
|
+
v3Only: v3Only.sort(),
|
|
251
|
+
v2Only: v2Only.sort(),
|
|
252
|
+
laneBySlug: Object.fromEntries(laneBySlug),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const n = turns.length;
|
|
257
|
+
const sum = (pick: (t: ShadowDiffTurn) => number): number =>
|
|
258
|
+
turns.reduce((acc, t) => acc + pick(t), 0);
|
|
259
|
+
const mean = (pick: (t: ShadowDiffTurn) => number): number =>
|
|
260
|
+
n === 0 ? 0 : sum(pick) / n;
|
|
261
|
+
|
|
262
|
+
// Newest-first for the detail listing; aggregates are order-independent.
|
|
263
|
+
const detail = [...turns]
|
|
264
|
+
.sort((a, b) => b.shadowAt - a.shadowAt)
|
|
265
|
+
.slice(0, detailLimit);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
toleranceMs,
|
|
269
|
+
shadowRows: shadow.length,
|
|
270
|
+
turnsCompared: n,
|
|
271
|
+
unpaired,
|
|
272
|
+
agg: {
|
|
273
|
+
meanV2: mean((t) => t.v2Count),
|
|
274
|
+
meanV3: mean((t) => t.v3Count),
|
|
275
|
+
meanOverlap: mean((t) => t.overlap.length),
|
|
276
|
+
meanJaccard: mean((t) => t.jaccard),
|
|
277
|
+
totalOverlap: sum((t) => t.overlap.length),
|
|
278
|
+
totalV3Only: sum((t) => t.v3Only.length),
|
|
279
|
+
totalV2Only: sum((t) => t.v2Only.length),
|
|
280
|
+
v3OnlyByLane,
|
|
281
|
+
overlapByLane,
|
|
282
|
+
v2OnlyTop: topSlugs(v2OnlyFreq, TOP_FREQUENCY_LIMIT),
|
|
283
|
+
v3OnlyTop: topSlugs(v3OnlyFreq, TOP_FREQUENCY_LIMIT),
|
|
284
|
+
},
|
|
285
|
+
turns: detail,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
PluginExecutionError,
|
|
35
35
|
} from "../../plugins/types.js";
|
|
36
36
|
import type { ContentBlock } from "../../providers/types.js";
|
|
37
|
+
import { isUntrustedTrustClass } from "../../runtime/actor-trust-resolver.js";
|
|
37
38
|
import { getLogger } from "../../util/logger.js";
|
|
38
39
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
39
40
|
import type { DrizzleDb } from "../db-connection.js";
|
|
@@ -151,10 +152,13 @@ const SHADOW_CONFIG_SNAPSHOT: MemoryV2ConfigSnapshot = {
|
|
|
151
152
|
* becomes a zeroed concept row tagged `source: "router"` and
|
|
152
153
|
* `status: "injected"` — the shadow has no activation scores to record, and the
|
|
153
154
|
* `mode='v3_shadow'` row tag (not the concept source) is what distinguishes
|
|
154
|
-
* shadow telemetry from live router selections.
|
|
155
|
+
* shadow telemetry from live router selections. Each row also carries the
|
|
156
|
+
* `lane` that surfaced the slug (from `sourceBySlug`) so a shadow run can be
|
|
157
|
+
* analyzed by provenance.
|
|
155
158
|
*/
|
|
156
159
|
function buildShadowConceptRows(
|
|
157
160
|
selectedSlugs: readonly string[],
|
|
161
|
+
sourceBySlug: ReadonlyMap<string, string>,
|
|
158
162
|
): MemoryV2ConceptRowRecord[] {
|
|
159
163
|
return selectedSlugs.map((slug) => ({
|
|
160
164
|
slug,
|
|
@@ -170,6 +174,7 @@ function buildShadowConceptRows(
|
|
|
170
174
|
spreadContribution: 0,
|
|
171
175
|
source: "router",
|
|
172
176
|
status: "injected",
|
|
177
|
+
...(sourceBySlug.get(slug) ? { lane: sourceBySlug.get(slug) } : {}),
|
|
173
178
|
}));
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -232,11 +237,37 @@ async function runShadowAndLog(
|
|
|
232
237
|
|
|
233
238
|
if (signal.aborted) return;
|
|
234
239
|
|
|
240
|
+
// Per-turn summary so a shadow run is analyzable from the logs — the pool
|
|
241
|
+
// shape, how many passes ran, and the gate's verdict + rationale. The
|
|
242
|
+
// selection set + per-slug lane land in the activation log below.
|
|
243
|
+
const passes = output.trace?.passes ?? [];
|
|
244
|
+
const lastGate = passes[passes.length - 1]?.gate;
|
|
245
|
+
const laneTally: Record<string, number> = {};
|
|
246
|
+
for (const lane of output.sourceBySlug.values()) {
|
|
247
|
+
laneTally[lane] = (laneTally[lane] ?? 0) + 1;
|
|
248
|
+
}
|
|
249
|
+
log.info(
|
|
250
|
+
{
|
|
251
|
+
conversationId: args.conversationId,
|
|
252
|
+
turn: args.turnIndex,
|
|
253
|
+
selected: output.selectedSlugs.length,
|
|
254
|
+
poolSize: output.sourceBySlug.size,
|
|
255
|
+
laneTally,
|
|
256
|
+
passes: passes.length,
|
|
257
|
+
gateDecision: lastGate?.decision,
|
|
258
|
+
gateReasoning: lastGate?.reasoning,
|
|
259
|
+
},
|
|
260
|
+
"v3 shadow selection",
|
|
261
|
+
);
|
|
262
|
+
|
|
235
263
|
recordMemoryV2ActivationLog({
|
|
236
264
|
conversationId: args.conversationId,
|
|
237
265
|
turn: args.turnIndex,
|
|
238
266
|
mode: "v3_shadow",
|
|
239
|
-
concepts: buildShadowConceptRows(
|
|
267
|
+
concepts: buildShadowConceptRows(
|
|
268
|
+
output.selectedSlugs,
|
|
269
|
+
output.sourceBySlug,
|
|
270
|
+
),
|
|
240
271
|
config: SHADOW_CONFIG_SNAPSHOT,
|
|
241
272
|
});
|
|
242
273
|
} catch (err) {
|
|
@@ -253,6 +284,11 @@ async function runShadowAndLog(
|
|
|
253
284
|
* Flag-gated INSIDE the middleware (per-turn, live-toggle): when v3 shadow is
|
|
254
285
|
* off it is a pure pass-through. When on, it fires the v3 loop detached and
|
|
255
286
|
* returns the unchanged downstream (v2) result immediately.
|
|
287
|
+
*
|
|
288
|
+
* The shadow loop spends filter + gate LLM calls, so — like the other
|
|
289
|
+
* guardian-trust background memory loops (`enqueueAutoAnalysisOnCompaction`,
|
|
290
|
+
* `enqueueMemoryRetrospectiveOnCompaction`) — it is gated on actor trust: an
|
|
291
|
+
* untrusted turn passes through without kicking off the v3 loop.
|
|
256
292
|
*/
|
|
257
293
|
export const memoryV3ShadowMiddleware: Middleware<MemoryArgs, MemoryResult> =
|
|
258
294
|
async function memoryV3Shadow(args, next) {
|
|
@@ -262,6 +298,12 @@ export const memoryV3ShadowMiddleware: Middleware<MemoryArgs, MemoryResult> =
|
|
|
262
298
|
return next(args);
|
|
263
299
|
}
|
|
264
300
|
|
|
301
|
+
if (isUntrustedTrustClass(args.trustContext?.trustClass)) {
|
|
302
|
+
// Untrusted actor: don't spend shadow retrieval LLM calls — mirrors the
|
|
303
|
+
// live path's trust gate. Pure pass-through, no detached work.
|
|
304
|
+
return next(args);
|
|
305
|
+
}
|
|
306
|
+
|
|
265
307
|
// Detached — never awaited on the path that returns the result, so the
|
|
266
308
|
// shadow can neither block nor slow the live turn. Errors are swallowed
|
|
267
309
|
// inside `runShadowAndLog`.
|
|
@@ -165,13 +165,18 @@ export async function walkTree(
|
|
|
165
165
|
const offeredRefs = new Set(offeredNodes.map((c) => c.ref));
|
|
166
166
|
|
|
167
167
|
// Honor the descend pick in the order it was returned, dedup'd, filtered
|
|
168
|
-
// to genuinely-offered node children,
|
|
168
|
+
// to genuinely-offered node children, skipping nodes already visited
|
|
169
|
+
// elsewhere in the DAG, and capped by `breadthBudget`. Filtering visited
|
|
170
|
+
// nodes *before* the budget check ensures the budget is spent only on
|
|
171
|
+
// nodes the walk will actually descend — an already-visited pick must not
|
|
172
|
+
// consume a slot that an unvisited sibling could use.
|
|
169
173
|
const descended: string[] = [];
|
|
170
174
|
const descendedSet = new Set<string>();
|
|
171
175
|
for (const choice of result.descend) {
|
|
172
176
|
if (choice.kind !== "node") continue;
|
|
173
177
|
if (!offeredRefs.has(choice.ref)) continue;
|
|
174
178
|
if (descendedSet.has(choice.ref)) continue;
|
|
179
|
+
if (visited.has(nodeKey(choice.ref))) continue;
|
|
175
180
|
if (descended.length >= breadthBudget) break;
|
|
176
181
|
descendedSet.add(choice.ref);
|
|
177
182
|
descended.push(choice.ref);
|
|
@@ -320,8 +320,13 @@ export function createDescender(args: CreateDescenderArgs): DescendDecision {
|
|
|
320
320
|
parsed.data.descend,
|
|
321
321
|
new Map(offeredNodes.map((c) => [c.ref, c])),
|
|
322
322
|
);
|
|
323
|
+
// Recall-safe fallback: an *omitted* `keep_pages` means the model gave no
|
|
324
|
+
// instruction at this node, so keep every offered page — dropping all pages
|
|
325
|
+
// a node presented on a silent omission is the worse failure. An *explicit*
|
|
326
|
+
// `[]` is the model genuinely keeping nothing here and is honored as-is.
|
|
327
|
+
const keepSlugs = parsed.data.keep_pages ?? offeredPageSlugs;
|
|
323
328
|
const keep = resolveOffered(
|
|
324
|
-
|
|
329
|
+
keepSlugs,
|
|
325
330
|
new Map(offeredPages.map((c) => [c.ref, c])),
|
|
326
331
|
);
|
|
327
332
|
return { descend, keep, reasoning: parsed.data.reasoning ?? "" };
|