@vellumai/assistant 0.8.10 → 0.8.11-staging.1
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/bun.lock +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for `orchestrate.ts` (
|
|
2
|
+
* Tests for `orchestrate.ts` (cache-ordered candidate pool).
|
|
3
3
|
*
|
|
4
|
-
* The orchestrator composes
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The orchestrator composes the stable-prefix lanes (curated core + frecency
|
|
5
|
+
* hot, both computed at lane init and passed in) with three deterministic
|
|
6
|
+
* finder lanes — the section-grain BM25 needle, the dense lane, and link-graph
|
|
7
|
+
* edge expansion — into ONE cache-ordered pool, then runs a SINGLE forced-tool
|
|
8
|
+
* select over it. The result is this turn's selections only; cross-turn
|
|
9
|
+
* persistence is the injector's job, not the orchestrator's.
|
|
10
10
|
*
|
|
11
11
|
* The select provider is stubbed (no network); a single stub answers the one
|
|
12
12
|
* `select_pages` call per turn by reading the numbered `<candidates>` block.
|
|
13
13
|
* The dense lane is stubbed at the module boundary. The needle and edge graph
|
|
14
|
-
* are real, built from tiny inline fixtures so the pool
|
|
14
|
+
* are real, built from tiny inline fixtures so the pool assembly is exercised
|
|
15
15
|
* end-to-end.
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -23,8 +23,10 @@ import type {
|
|
|
23
23
|
Provider,
|
|
24
24
|
ProviderResponse,
|
|
25
25
|
} from "../../../../providers/types.js";
|
|
26
|
+
import { renderCard } from "../card.js";
|
|
26
27
|
import type { EdgeGraph } from "../edge.js";
|
|
27
28
|
import { buildEdgeGraph } from "../edge.js";
|
|
29
|
+
import type { OrchestrateDeps } from "../orchestrate.js";
|
|
28
30
|
import { buildSectionNeedle } from "../section-needle.js";
|
|
29
31
|
import { buildSectionIndex } from "../sections.js";
|
|
30
32
|
import type { MemoryRoutingTurn, SectionIndex, Slug } from "../types.js";
|
|
@@ -66,7 +68,6 @@ mock.module("../dense.js", () => ({
|
|
|
66
68
|
|
|
67
69
|
const { orchestrate, DEFAULT_NEEDLE_K, DEFAULT_DENSE_K } =
|
|
68
70
|
await import("../orchestrate.js");
|
|
69
|
-
const { WorkingSet } = await import("../working-set.js");
|
|
70
71
|
|
|
71
72
|
// ---------------------------------------------------------------------------
|
|
72
73
|
// Fixtures: a tiny corpus of pages with bodies + `links:` frontmatter.
|
|
@@ -100,11 +101,13 @@ function makeEntries(): PageIndexEntry[] {
|
|
|
100
101
|
}));
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
interface Lanes {
|
|
104
105
|
sectionIndex: SectionIndex;
|
|
105
106
|
needle: ReturnType<typeof buildSectionNeedle>;
|
|
106
107
|
edgeGraph: EdgeGraph;
|
|
107
|
-
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function buildLanes(): Promise<Lanes> {
|
|
108
111
|
const sectionIndex = await buildSectionIndex(SLUGS, async (s) => PAGES[s]!);
|
|
109
112
|
const needle = buildSectionNeedle(sectionIndex);
|
|
110
113
|
const edgeGraph = await buildEdgeGraph(makeEntries(), async (s) => RAW[s]!);
|
|
@@ -113,6 +116,35 @@ async function buildLanes(): Promise<{
|
|
|
113
116
|
|
|
114
117
|
const config = {} as never;
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Orchestrate deps with empty stable-prefix lanes unless overridden. Mirrors
|
|
121
|
+
* lane init: every core/hot slug gets a pre-rendered card (from the RAW
|
|
122
|
+
* fixture when one exists) unless the test overrides `prefixCards` itself.
|
|
123
|
+
*/
|
|
124
|
+
function depsOf(
|
|
125
|
+
lanes: Lanes,
|
|
126
|
+
overrides: Partial<OrchestrateDeps> = {},
|
|
127
|
+
): OrchestrateDeps {
|
|
128
|
+
const coreSlugs = overrides.coreSlugs ?? [];
|
|
129
|
+
const hotSlugs = overrides.hotSlugs ?? [];
|
|
130
|
+
const prefixCards = new Map<Slug, string>(
|
|
131
|
+
[...coreSlugs, ...hotSlugs].map((slug) => [
|
|
132
|
+
slug,
|
|
133
|
+
renderCard(slug, RAW[slug] ?? ""),
|
|
134
|
+
]),
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
sectionIndex: lanes.sectionIndex,
|
|
138
|
+
needle: lanes.needle,
|
|
139
|
+
denseConfig: config,
|
|
140
|
+
edgeGraph: lanes.edgeGraph,
|
|
141
|
+
coreSlugs,
|
|
142
|
+
hotSlugs,
|
|
143
|
+
prefixCards,
|
|
144
|
+
...overrides,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
116
148
|
function makeTurn(
|
|
117
149
|
turnNumber: number,
|
|
118
150
|
currentMessage: string,
|
|
@@ -134,39 +166,74 @@ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
|
|
|
134
166
|
};
|
|
135
167
|
}
|
|
136
168
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Parse the two-segment selector input back into the globally-numbered pool:
|
|
171
|
+
* stable-prefix cards (`<candidate_cards>`, identified by their
|
|
172
|
+
* `[i] # memory/concepts/<slug>.md` header line) and finder lines
|
|
173
|
+
* (`<candidates>`, `[i] slug — descriptor`). Also captures the raw stable
|
|
174
|
+
* prefix block for byte-identity assertions.
|
|
175
|
+
*/
|
|
176
|
+
function parsePool(messages: Message[]): {
|
|
177
|
+
slugs: Slug[];
|
|
178
|
+
lines: string[];
|
|
179
|
+
prefixBlock: string | null;
|
|
180
|
+
} {
|
|
181
|
+
const entries: Array<{ id: number; slug: string; line: string }> = [];
|
|
182
|
+
let prefixBlock: string | null = null;
|
|
139
183
|
for (const msg of messages) {
|
|
140
184
|
for (const block of msg.content) {
|
|
141
185
|
if (block.type !== "text") continue;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
186
|
+
const cards = /<candidate_cards>\n([\s\S]*?)\n<\/candidate_cards>/.exec(
|
|
187
|
+
block.text,
|
|
188
|
+
);
|
|
189
|
+
if (cards) {
|
|
190
|
+
prefixBlock = cards[0];
|
|
191
|
+
for (const m of cards[1].matchAll(
|
|
192
|
+
/^\[(\d+)\] # memory\/concepts\/(.+)\.md$/gm,
|
|
193
|
+
)) {
|
|
194
|
+
entries.push({ id: Number(m[1]), slug: m[2]!, line: m[0] });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const finder = /<candidates>\n([\s\S]*?)\n<\/candidates>/.exec(
|
|
198
|
+
block.text,
|
|
199
|
+
);
|
|
200
|
+
if (finder) {
|
|
201
|
+
for (const line of finder[1].split("\n")) {
|
|
202
|
+
const m = /^\[(\d+)\] (\S+)(?: — |$)/.exec(line);
|
|
203
|
+
if (m) entries.push({ id: Number(m[1]), slug: m[2]!, line });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
148
206
|
}
|
|
149
207
|
}
|
|
150
|
-
|
|
208
|
+
entries.sort((a, b) => a.id - b.id);
|
|
209
|
+
return {
|
|
210
|
+
slugs: entries.map((e) => e.slug),
|
|
211
|
+
lines: entries.map((e) => e.line),
|
|
212
|
+
prefixBlock,
|
|
213
|
+
};
|
|
151
214
|
}
|
|
152
215
|
|
|
153
216
|
/**
|
|
154
217
|
* Provider that selects the pool candidates whose slug is in `keep` (mapping
|
|
155
218
|
* each back to its 1-based id), pinning those in `pin`. Captures the rendered
|
|
156
|
-
* candidate list for pool assertions.
|
|
219
|
+
* candidate list (slugs and raw lines) for pool assertions.
|
|
157
220
|
*/
|
|
158
221
|
let lastPool: Slug[] = [];
|
|
222
|
+
let lastPoolLines: string[] = [];
|
|
223
|
+
let lastPrefixBlock: string | null = null;
|
|
159
224
|
let selectCalls = 0;
|
|
160
225
|
function selectProvider(keep: Slug[], pin: Slug[] = []): Provider {
|
|
161
226
|
return {
|
|
162
227
|
name: "stub",
|
|
163
228
|
sendMessage: async (messages) => {
|
|
164
229
|
selectCalls++;
|
|
165
|
-
const
|
|
166
|
-
lastPool =
|
|
230
|
+
const parsed = parsePool(messages);
|
|
231
|
+
lastPool = parsed.slugs;
|
|
232
|
+
lastPoolLines = parsed.lines;
|
|
233
|
+
lastPrefixBlock = parsed.prefixBlock;
|
|
167
234
|
const ids: number[] = [];
|
|
168
235
|
const pinned_ids: number[] = [];
|
|
169
|
-
|
|
236
|
+
parsed.slugs.forEach((slug, i) => {
|
|
170
237
|
if (keep.includes(slug)) ids.push(i + 1);
|
|
171
238
|
if (pin.includes(slug)) pinned_ids.push(i + 1);
|
|
172
239
|
});
|
|
@@ -180,6 +247,8 @@ beforeEach(() => {
|
|
|
180
247
|
providerStub = null;
|
|
181
248
|
denseHits = [];
|
|
182
249
|
lastPool = [];
|
|
250
|
+
lastPoolLines = [];
|
|
251
|
+
lastPrefixBlock = null;
|
|
183
252
|
selectCalls = 0;
|
|
184
253
|
});
|
|
185
254
|
|
|
@@ -188,9 +257,9 @@ afterAll(() => {
|
|
|
188
257
|
});
|
|
189
258
|
|
|
190
259
|
// ---------------------------------------------------------------------------
|
|
191
|
-
// Pool composition: the candidate pool is the union of the
|
|
192
|
-
// capability pages are
|
|
193
|
-
// the dedicated test below).
|
|
260
|
+
// Pool composition: the candidate pool is the cache-ordered union of the
|
|
261
|
+
// lanes. Synthetic capability pages are not always-added — they enter through
|
|
262
|
+
// a lane (see the dedicated test below).
|
|
194
263
|
// ---------------------------------------------------------------------------
|
|
195
264
|
|
|
196
265
|
describe("orchestrate — candidate pool composition", () => {
|
|
@@ -201,13 +270,7 @@ describe("orchestrate — candidate pool composition", () => {
|
|
|
201
270
|
denseHits = [{ article: "topic-b", section: 0 }];
|
|
202
271
|
providerStub = selectProvider([]); // selection is irrelevant to pool union
|
|
203
272
|
|
|
204
|
-
await orchestrate(makeTurn(1, "apple"),
|
|
205
|
-
sectionIndex: lanes.sectionIndex,
|
|
206
|
-
needle: lanes.needle,
|
|
207
|
-
denseConfig: config,
|
|
208
|
-
edgeGraph: lanes.edgeGraph,
|
|
209
|
-
workingSet: new WorkingSet(),
|
|
210
|
-
});
|
|
273
|
+
await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
211
274
|
|
|
212
275
|
expect(selectCalls).toBe(1);
|
|
213
276
|
expect(new Set(lastPool)).toEqual(
|
|
@@ -230,13 +293,10 @@ describe("orchestrate — candidate pool composition", () => {
|
|
|
230
293
|
const edgeGraph = await buildEdgeGraph(makeEntries(), async (s) => RAW[s]!);
|
|
231
294
|
|
|
232
295
|
providerStub = selectProvider([]); // selection irrelevant to pool union
|
|
233
|
-
await orchestrate(
|
|
234
|
-
|
|
235
|
-
needle,
|
|
236
|
-
|
|
237
|
-
edgeGraph,
|
|
238
|
-
workingSet: new WorkingSet(),
|
|
239
|
-
});
|
|
296
|
+
await orchestrate(
|
|
297
|
+
makeTurn(1, "kumquat"),
|
|
298
|
+
depsOf({ sectionIndex, needle, edgeGraph }),
|
|
299
|
+
);
|
|
240
300
|
|
|
241
301
|
// The needle ranked the capability page on the "kumquat" term, so it is in
|
|
242
302
|
// the candidate pool.
|
|
@@ -245,30 +305,12 @@ describe("orchestrate — candidate pool composition", () => {
|
|
|
245
305
|
|
|
246
306
|
test("edge curated link description becomes the edge candidate's descriptor", async () => {
|
|
247
307
|
const lanes = await buildLanes();
|
|
248
|
-
|
|
249
|
-
providerStub = {
|
|
250
|
-
name: "stub",
|
|
251
|
-
sendMessage: async (messages) => {
|
|
252
|
-
for (const msg of messages) {
|
|
253
|
-
for (const block of msg.content) {
|
|
254
|
-
if (block.type !== "text") continue;
|
|
255
|
-
const m = /\[\d+\] topic-d — ([^\n]+)/.exec(block.text);
|
|
256
|
-
if (m) descriptorLine = m[1]!;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return toolUseResponse({ ids: [] });
|
|
260
|
-
},
|
|
261
|
-
};
|
|
308
|
+
providerStub = selectProvider([]);
|
|
262
309
|
|
|
263
|
-
await orchestrate(makeTurn(1, "apple"),
|
|
264
|
-
sectionIndex: lanes.sectionIndex,
|
|
265
|
-
needle: lanes.needle,
|
|
266
|
-
denseConfig: config,
|
|
267
|
-
edgeGraph: lanes.edgeGraph,
|
|
268
|
-
workingSet: new WorkingSet(),
|
|
269
|
-
});
|
|
310
|
+
await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
270
311
|
|
|
271
|
-
|
|
312
|
+
const line = lastPoolLines.find((l) => / topic-d — /.test(l));
|
|
313
|
+
expect(line).toContain("the curated edge from a to d");
|
|
272
314
|
});
|
|
273
315
|
|
|
274
316
|
test("needleK and denseK default to their constants", async () => {
|
|
@@ -282,31 +324,187 @@ describe("orchestrate — candidate pool composition", () => {
|
|
|
282
324
|
bestSection: () => -1,
|
|
283
325
|
};
|
|
284
326
|
providerStub = selectProvider([]);
|
|
285
|
-
await orchestrate(makeTurn(1, "x"), {
|
|
286
|
-
sectionIndex: lanes.sectionIndex,
|
|
287
|
-
needle,
|
|
288
|
-
denseConfig: config,
|
|
289
|
-
edgeGraph: lanes.edgeGraph,
|
|
290
|
-
workingSet: new WorkingSet(),
|
|
291
|
-
});
|
|
327
|
+
await orchestrate(makeTurn(1, "x"), depsOf(lanes, { needle }));
|
|
292
328
|
expect(needleK).toBe(DEFAULT_NEEDLE_K);
|
|
293
329
|
expect(DEFAULT_DENSE_K).toBe(100);
|
|
294
330
|
});
|
|
295
331
|
|
|
296
|
-
test("
|
|
332
|
+
test("matchedSections is populated from matched lane sections", async () => {
|
|
297
333
|
const lanes = await buildLanes();
|
|
298
334
|
denseHits = [];
|
|
299
335
|
providerStub = selectProvider(["topic-a"]);
|
|
300
|
-
const result = await orchestrate(makeTurn(1, "apple"),
|
|
301
|
-
sectionIndex: lanes.sectionIndex,
|
|
302
|
-
needle: lanes.needle,
|
|
303
|
-
denseConfig: config,
|
|
304
|
-
edgeGraph: lanes.edgeGraph,
|
|
305
|
-
workingSet: new WorkingSet(),
|
|
306
|
-
});
|
|
336
|
+
const result = await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
307
337
|
// topic-a matched "apple" in its `## Details` section.
|
|
308
|
-
expect(result.
|
|
309
|
-
expect(result.
|
|
338
|
+
expect(result.matchedSections.get("topic-a")?.article).toBe("topic-a");
|
|
339
|
+
expect(result.matchedSections.get("topic-a")?.text).toContain("apple");
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// Cache order: the pool's stable prefix is core (file order) then hot (score
|
|
345
|
+
// order); finder candidates follow, deduped against the prefix. The prefix is
|
|
346
|
+
// byte-identical across turns while the lanes are unchanged — that is the
|
|
347
|
+
// whole point of the ordering (selector-input KV cache).
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
describe("orchestrate — cache-ordered pool (core + hot + finders)", () => {
|
|
351
|
+
test("pool order is core, then hot, then finder candidates", async () => {
|
|
352
|
+
const lanes = await buildLanes();
|
|
353
|
+
// Core and hot pages do not match "apple"; the needle surfaces topic-a.
|
|
354
|
+
// topic-a links to topic-d, but topic-d is HOT (stable prefix), so the
|
|
355
|
+
// edge lane does not re-surface it.
|
|
356
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
357
|
+
providerStub = selectProvider([]);
|
|
358
|
+
|
|
359
|
+
const result = await orchestrate(
|
|
360
|
+
makeTurn(1, "apple"),
|
|
361
|
+
depsOf(lanes, { coreSlugs: ["topic-c"], hotSlugs: ["topic-d"] }),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
expect(lastPool).toEqual(["topic-c", "topic-d", "topic-a", "topic-b"]);
|
|
365
|
+
expect(result.lanes.core).toEqual(["topic-c"]);
|
|
366
|
+
expect(result.lanes.hot).toEqual(["topic-d"]);
|
|
367
|
+
// The edge lane skipped topic-d (already in the stable prefix).
|
|
368
|
+
expect(result.lanes.finder.map((c) => c.slug)).toEqual([
|
|
369
|
+
"topic-a",
|
|
370
|
+
"topic-b",
|
|
371
|
+
]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("the rendered stable prefix is byte-identical across turns with different queries", async () => {
|
|
375
|
+
const lanes = await buildLanes();
|
|
376
|
+
const deps = depsOf(lanes, {
|
|
377
|
+
coreSlugs: ["topic-c"],
|
|
378
|
+
hotSlugs: ["topic-b"],
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
providerStub = selectProvider([]);
|
|
382
|
+
await orchestrate(makeTurn(1, "apple"), deps);
|
|
383
|
+
const prefix1 = lastPrefixBlock;
|
|
384
|
+
|
|
385
|
+
await orchestrate(makeTurn(2, "grape"), deps);
|
|
386
|
+
const prefix2 = lastPrefixBlock;
|
|
387
|
+
|
|
388
|
+
// Stable-prefix cards are pre-rendered and query-independent, so the
|
|
389
|
+
// whole rendered cards block matches byte-for-byte across turns.
|
|
390
|
+
expect(prefix1).not.toBeNull();
|
|
391
|
+
expect(prefix2).toBe(prefix1!);
|
|
392
|
+
expect(prefix1).toContain("topic-c");
|
|
393
|
+
expect(prefix1).toContain("topic-b");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("stable-prefix candidates render their pre-rendered cards verbatim", async () => {
|
|
397
|
+
const lanes = await buildLanes();
|
|
398
|
+
providerStub = selectProvider([]);
|
|
399
|
+
|
|
400
|
+
await orchestrate(
|
|
401
|
+
makeTurn(1, "zzzz"),
|
|
402
|
+
depsOf(lanes, {
|
|
403
|
+
coreSlugs: ["topic-c"],
|
|
404
|
+
hotSlugs: ["topic-d"],
|
|
405
|
+
prefixCards: new Map([
|
|
406
|
+
[
|
|
407
|
+
"topic-c",
|
|
408
|
+
"# memory/concepts/topic-c.md\nlead for topic c\n\n[sections: §Notes]",
|
|
409
|
+
],
|
|
410
|
+
["topic-d", "# memory/concepts/topic-d.md\nlead for topic d"],
|
|
411
|
+
]),
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(lastPrefixBlock).toContain(
|
|
416
|
+
"[1] # memory/concepts/topic-c.md\nlead for topic c\n\n[sections: §Notes]",
|
|
417
|
+
);
|
|
418
|
+
expect(lastPrefixBlock).toContain(
|
|
419
|
+
"[2] # memory/concepts/topic-d.md\nlead for topic d",
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("a stable-prefix slug with no pre-rendered card throws (never silently degrades)", async () => {
|
|
424
|
+
const lanes = await buildLanes();
|
|
425
|
+
providerStub = selectProvider([]);
|
|
426
|
+
|
|
427
|
+
await expect(
|
|
428
|
+
orchestrate(
|
|
429
|
+
makeTurn(1, "zzzz"),
|
|
430
|
+
depsOf(lanes, {
|
|
431
|
+
coreSlugs: ["topic-c"],
|
|
432
|
+
hotSlugs: ["topic-d"],
|
|
433
|
+
// topic-d is missing — a lane-init bug; a degraded card would break
|
|
434
|
+
// the byte-stable-prefix contract, so orchestrate must throw.
|
|
435
|
+
prefixCards: new Map([["topic-c", renderCard("topic-c", "")]]),
|
|
436
|
+
}),
|
|
437
|
+
),
|
|
438
|
+
).rejects.toThrow('no pre-rendered card for stable-prefix slug "topic-d"');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("a finder hit on a core page repeats as a finder line and dedupes on selection", async () => {
|
|
442
|
+
const lanes = await buildLanes();
|
|
443
|
+
// topic-a is CORE and the needle also hits it on "apple". The stub keeps
|
|
444
|
+
// BOTH occurrences (card id + finder-line id).
|
|
445
|
+
providerStub = selectProvider(["topic-a"]);
|
|
446
|
+
|
|
447
|
+
const result = await orchestrate(
|
|
448
|
+
makeTurn(1, "apple"),
|
|
449
|
+
depsOf(lanes, { coreSlugs: ["topic-a"] }),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// The pool lists topic-a twice — once as the stable-prefix card, once as
|
|
453
|
+
// a finder line carrying its CURRENT matched section (the tail is not
|
|
454
|
+
// deduped against the prefix, by design: filtering would not change the
|
|
455
|
+
// prefix here, but the snippet line is the page's current relevance).
|
|
456
|
+
expect(lastPool.filter((s) => s === "topic-a")).toHaveLength(2);
|
|
457
|
+
expect(lastPool[0]).toBe("topic-a");
|
|
458
|
+
// The finder line shows the matched-section snippet.
|
|
459
|
+
expect(lastPoolLines.find((l) => /^\[2\] topic-a — /.test(l))).toContain(
|
|
460
|
+
"apple",
|
|
461
|
+
);
|
|
462
|
+
// Selecting both ids still yields ONE selection (slug dedup), the finder
|
|
463
|
+
// lane records the hit, and the matched section survives downstream.
|
|
464
|
+
expect(result.selections).toEqual([{ slug: "topic-a", pinned: false }]);
|
|
465
|
+
expect(result.lanes.finder.map((c) => c.slug)).toContain("topic-a");
|
|
466
|
+
expect(result.matchedSections.get("topic-a")?.text).toContain("apple");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("a hot slug duplicated into core is defensively dropped from hot", async () => {
|
|
470
|
+
const lanes = await buildLanes();
|
|
471
|
+
providerStub = selectProvider([]);
|
|
472
|
+
|
|
473
|
+
const result = await orchestrate(
|
|
474
|
+
makeTurn(1, "zzzz"),
|
|
475
|
+
depsOf(lanes, {
|
|
476
|
+
coreSlugs: ["topic-c"],
|
|
477
|
+
hotSlugs: ["topic-c", "topic-d"],
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
expect(result.lanes.core).toEqual(["topic-c"]);
|
|
482
|
+
expect(result.lanes.hot).toEqual(["topic-d"]);
|
|
483
|
+
expect(lastPool).toEqual(["topic-c", "topic-d"]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("selections are current-turn only — no carried set is unioned in", async () => {
|
|
487
|
+
const lanes = await buildLanes();
|
|
488
|
+
const deps = depsOf(lanes, { coreSlugs: ["topic-c"] });
|
|
489
|
+
|
|
490
|
+
// Turn 1 selects topic-a.
|
|
491
|
+
providerStub = selectProvider(["topic-a"]);
|
|
492
|
+
const t1 = await orchestrate(makeTurn(1, "apple"), deps);
|
|
493
|
+
expect(t1.selections.map((s) => s.slug)).toEqual(["topic-a"]);
|
|
494
|
+
|
|
495
|
+
// Turn 2 selects only topic-b; topic-a does NOT reappear (cross-turn
|
|
496
|
+
// persistence is the injector's job now, not orchestration's).
|
|
497
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
498
|
+
providerStub = selectProvider(["topic-b"]);
|
|
499
|
+
const t2 = await orchestrate(makeTurn(2, "cherry"), deps);
|
|
500
|
+
expect(t2.selections.map((s) => s.slug)).toEqual(["topic-b"]);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("pinned flags survive selection dedup", async () => {
|
|
504
|
+
const lanes = await buildLanes();
|
|
505
|
+
providerStub = selectProvider(["topic-a"], ["topic-a"]);
|
|
506
|
+
const result = await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
507
|
+
expect(result.selections).toEqual([{ slug: "topic-a", pinned: true }]);
|
|
310
508
|
});
|
|
311
509
|
});
|
|
312
510
|
|
|
@@ -319,53 +517,18 @@ describe("orchestrate — candidate pool composition", () => {
|
|
|
319
517
|
// ---------------------------------------------------------------------------
|
|
320
518
|
|
|
321
519
|
describe("orchestrate — edge-only injection", () => {
|
|
322
|
-
test("an edge-only page records NO
|
|
520
|
+
test("an edge-only page records NO matchedSections entry (→ full-page inject)", async () => {
|
|
323
521
|
const lanes = await buildLanes();
|
|
324
522
|
// "apple" hits topic-a (needle); topic-a links to topic-d (edge-only — the
|
|
325
523
|
// query never hits topic-d). Select topic-d so it is in the result.
|
|
326
524
|
denseHits = [];
|
|
327
525
|
providerStub = selectProvider(["topic-d"]);
|
|
328
|
-
const result = await orchestrate(makeTurn(1, "apple"),
|
|
329
|
-
sectionIndex: lanes.sectionIndex,
|
|
330
|
-
needle: lanes.needle,
|
|
331
|
-
denseConfig: config,
|
|
332
|
-
edgeGraph: lanes.edgeGraph,
|
|
333
|
-
workingSet: new WorkingSet(),
|
|
334
|
-
});
|
|
526
|
+
const result = await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
335
527
|
|
|
336
|
-
// topic-d was selected
|
|
528
|
+
// topic-d was selected, but with NO matched section — so
|
|
337
529
|
// `renderV3SectionContent(slug, undefined)` falls back to the full page.
|
|
338
|
-
expect(result.
|
|
339
|
-
expect(result.
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("an edge-only page still carries the curated links description as its descriptor", async () => {
|
|
343
|
-
const lanes = await buildLanes();
|
|
344
|
-
let descriptorLine = "";
|
|
345
|
-
providerStub = {
|
|
346
|
-
name: "stub",
|
|
347
|
-
sendMessage: async (messages) => {
|
|
348
|
-
for (const msg of messages) {
|
|
349
|
-
for (const block of msg.content) {
|
|
350
|
-
if (block.type !== "text") continue;
|
|
351
|
-
const m = /\[\d+\] topic-d — ([^\n]+)/.exec(block.text);
|
|
352
|
-
if (m) descriptorLine = m[1]!;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return toolUseResponse({ ids: [] });
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
await orchestrate(makeTurn(1, "apple"), {
|
|
360
|
-
sectionIndex: lanes.sectionIndex,
|
|
361
|
-
needle: lanes.needle,
|
|
362
|
-
denseConfig: config,
|
|
363
|
-
edgeGraph: lanes.edgeGraph,
|
|
364
|
-
workingSet: new WorkingSet(),
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// The curated `links` description (not the lead text) is the descriptor.
|
|
368
|
-
expect(descriptorLine).toContain("the curated edge from a to d");
|
|
530
|
+
expect(result.selections.map((s) => s.slug)).toContain("topic-d");
|
|
531
|
+
expect(result.matchedSections.has("topic-d")).toBe(false);
|
|
369
532
|
});
|
|
370
533
|
|
|
371
534
|
test("an edge-only page with no curated description falls back to bestSection text as the descriptor", async () => {
|
|
@@ -373,7 +536,7 @@ describe("orchestrate — edge-only injection", () => {
|
|
|
373
536
|
// candidate's descriptor falls back to the page's best section against the
|
|
374
537
|
// query. The query never hits dst-page, so bestSection returns its lead;
|
|
375
538
|
// that lead text becomes the descriptor (and the page is still injected in
|
|
376
|
-
// full, with no
|
|
539
|
+
// full, with no matchedSections entry).
|
|
377
540
|
const pages: Record<Slug, string> = {
|
|
378
541
|
"src-page": "lead for src\n## Body\nalpha bravo about src",
|
|
379
542
|
"dst-page": "lead content for dst page\n## Extra\nnothing relevant here",
|
|
@@ -396,34 +559,19 @@ describe("orchestrate — edge-only injection", () => {
|
|
|
396
559
|
const needle = buildSectionNeedle(sectionIndex);
|
|
397
560
|
const edgeGraph = await buildEdgeGraph(entries, async (s) => raw[s]!);
|
|
398
561
|
|
|
399
|
-
|
|
400
|
-
providerStub = {
|
|
401
|
-
name: "stub",
|
|
402
|
-
sendMessage: async (messages) => {
|
|
403
|
-
for (const msg of messages) {
|
|
404
|
-
for (const block of msg.content) {
|
|
405
|
-
if (block.type !== "text") continue;
|
|
406
|
-
const m = /\[\d+\] dst-page — ([^\n]+)/.exec(block.text);
|
|
407
|
-
if (m) descriptorLine = m[1]!;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return toolUseResponse({ ids: [] });
|
|
411
|
-
},
|
|
412
|
-
};
|
|
562
|
+
providerStub = selectProvider([]);
|
|
413
563
|
|
|
414
564
|
// "alpha" hits src-page (needle); src-page links to dst-page (edge-only, no
|
|
415
565
|
// curated description).
|
|
416
|
-
const result = await orchestrate(
|
|
417
|
-
|
|
418
|
-
needle,
|
|
419
|
-
|
|
420
|
-
edgeGraph,
|
|
421
|
-
workingSet: new WorkingSet(),
|
|
422
|
-
});
|
|
566
|
+
const result = await orchestrate(
|
|
567
|
+
makeTurn(1, "alpha"),
|
|
568
|
+
depsOf({ sectionIndex, needle, edgeGraph }),
|
|
569
|
+
);
|
|
423
570
|
|
|
424
571
|
// Descriptor fell back to dst-page's lead text; still no matched section.
|
|
425
|
-
|
|
426
|
-
expect(
|
|
572
|
+
const line = lastPoolLines.find((l) => / dst-page — /.test(l));
|
|
573
|
+
expect(line).toContain("lead content for dst page");
|
|
574
|
+
expect(result.matchedSections.has("dst-page")).toBe(false);
|
|
427
575
|
});
|
|
428
576
|
});
|
|
429
577
|
|
|
@@ -443,202 +591,76 @@ describe("orchestrate — dense liveness filter", () => {
|
|
|
443
591
|
{ article: "gone-page", section: 0 },
|
|
444
592
|
];
|
|
445
593
|
providerStub = selectProvider([]); // selection irrelevant to pool membership
|
|
446
|
-
const result = await orchestrate(makeTurn(1, "apple"),
|
|
447
|
-
sectionIndex: lanes.sectionIndex,
|
|
448
|
-
needle: lanes.needle,
|
|
449
|
-
denseConfig: config,
|
|
450
|
-
edgeGraph: lanes.edgeGraph,
|
|
451
|
-
workingSet: new WorkingSet(),
|
|
452
|
-
});
|
|
594
|
+
const result = await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
453
595
|
|
|
454
596
|
// The live dense hit is pooled; the deleted page is dropped entirely.
|
|
455
597
|
expect(lastPool).toContain("topic-b");
|
|
456
598
|
expect(lastPool).not.toContain("gone-page");
|
|
457
|
-
expect(result.
|
|
599
|
+
expect(result.lanes.finder.map((c) => c.slug)).not.toContain("gone-page");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("a dense hit with an unresolvable ordinal falls back to the lead-section snippet", async () => {
|
|
603
|
+
const lanes = await buildLanes();
|
|
604
|
+
// Ordinal 99 resolves to no section, so the candidate carries no match
|
|
605
|
+
// text — its finder line falls back to the page's lead-section text.
|
|
606
|
+
denseHits = [{ article: "topic-b", section: 99 }];
|
|
607
|
+
providerStub = selectProvider([]);
|
|
608
|
+
await orchestrate(makeTurn(1, "zzzz"), depsOf(lanes));
|
|
609
|
+
|
|
610
|
+
const line = lastPoolLines.find((l) => / topic-b — /.test(l));
|
|
611
|
+
expect(line).toContain("lead for topic b");
|
|
458
612
|
});
|
|
459
613
|
});
|
|
460
614
|
|
|
461
615
|
// ---------------------------------------------------------------------------
|
|
462
|
-
// Lane provenance: each
|
|
463
|
-
//
|
|
464
|
-
//
|
|
616
|
+
// Lane provenance: each finder candidate records the lane that FIRST surfaced
|
|
617
|
+
// it (needle → dense → edge precedence), exposed via `result.lanes.finder` so
|
|
618
|
+
// the selection telemetry can attribute true sources.
|
|
465
619
|
// ---------------------------------------------------------------------------
|
|
466
620
|
|
|
467
|
-
describe("orchestrate — lane provenance
|
|
468
|
-
test("
|
|
621
|
+
describe("orchestrate — finder lane provenance", () => {
|
|
622
|
+
test("each finder candidate is tagged with its surfacing lane", async () => {
|
|
469
623
|
const lanes = await buildLanes();
|
|
470
624
|
// "apple" hits topic-a (needle). Dense returns topic-b. topic-a links to
|
|
471
625
|
// topic-d (edge). So each lane contributes exactly one distinct slug.
|
|
472
626
|
denseHits = [{ article: "topic-b", section: 0 }];
|
|
473
627
|
providerStub = selectProvider([]); // selection irrelevant to pool provenance
|
|
474
|
-
const result = await orchestrate(makeTurn(1, "apple"),
|
|
475
|
-
sectionIndex: lanes.sectionIndex,
|
|
476
|
-
needle: lanes.needle,
|
|
477
|
-
denseConfig: config,
|
|
478
|
-
edgeGraph: lanes.edgeGraph,
|
|
479
|
-
workingSet: new WorkingSet(),
|
|
480
|
-
});
|
|
628
|
+
const result = await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
481
629
|
|
|
482
|
-
|
|
483
|
-
expect(
|
|
484
|
-
expect(
|
|
630
|
+
const laneOf = new Map(result.lanes.finder.map((c) => [c.slug, c.lane]));
|
|
631
|
+
expect(laneOf.get("topic-a")).toBe("needle");
|
|
632
|
+
expect(laneOf.get("topic-b")).toBe("dense");
|
|
633
|
+
expect(laneOf.get("topic-d")).toBe("edge");
|
|
485
634
|
});
|
|
486
635
|
|
|
487
636
|
test("a slug surfaced by needle AND dense keeps the needle lane (first wins)", async () => {
|
|
488
637
|
const lanes = await buildLanes();
|
|
489
638
|
// topic-a is surfaced by the needle on "apple"; dense ALSO returns topic-a.
|
|
490
|
-
// Needle runs first
|
|
639
|
+
// Needle runs first, so the recorded lane stays needle.
|
|
491
640
|
denseHits = [{ article: "topic-a", section: 0 }];
|
|
492
641
|
providerStub = selectProvider([]);
|
|
493
|
-
const result = await orchestrate(makeTurn(1, "apple"),
|
|
494
|
-
sectionIndex: lanes.sectionIndex,
|
|
495
|
-
needle: lanes.needle,
|
|
496
|
-
denseConfig: config,
|
|
497
|
-
edgeGraph: lanes.edgeGraph,
|
|
498
|
-
workingSet: new WorkingSet(),
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
expect(result.laneBySlug.get("topic-a")).toBe("needle");
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// ---------------------------------------------------------------------------
|
|
506
|
-
// Carry-forward: same working-set semantics as the prior tree pipeline.
|
|
507
|
-
// ---------------------------------------------------------------------------
|
|
508
|
-
|
|
509
|
-
describe("orchestrate — carry-forward", () => {
|
|
510
|
-
test("a page selected+pinned in turn 1 carries into turn 2 without re-selection", async () => {
|
|
511
|
-
const lanes = await buildLanes();
|
|
512
|
-
const workingSet = new WorkingSet();
|
|
513
|
-
|
|
514
|
-
// Turn 1 selects+pins topic-a.
|
|
515
|
-
providerStub = selectProvider(["topic-a"], ["topic-a"]);
|
|
516
|
-
await orchestrate(makeTurn(1, "apple"), {
|
|
517
|
-
sectionIndex: lanes.sectionIndex,
|
|
518
|
-
needle: lanes.needle,
|
|
519
|
-
denseConfig: config,
|
|
520
|
-
edgeGraph: lanes.edgeGraph,
|
|
521
|
-
workingSet,
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
// Turn 2 selects a DIFFERENT page and never re-selects topic-a.
|
|
525
|
-
denseHits = [{ article: "topic-b", section: 0 }];
|
|
526
|
-
providerStub = selectProvider(["topic-b"]);
|
|
527
|
-
const t2 = await orchestrate(makeTurn(2, "cherry"), {
|
|
528
|
-
sectionIndex: lanes.sectionIndex,
|
|
529
|
-
needle: lanes.needle,
|
|
530
|
-
denseConfig: config,
|
|
531
|
-
edgeGraph: lanes.edgeGraph,
|
|
532
|
-
workingSet,
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
expect(t2.currentSelections.map((s) => s.slug)).not.toContain("topic-a");
|
|
536
|
-
expect(t2.finalInjection).toContain("topic-a");
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
test("carry-forward survives a turn whose selections fill the cap", async () => {
|
|
540
|
-
const lanes = await buildLanes();
|
|
541
|
-
// Cap of 1: under a naive record-then-cap order this turn's own selection
|
|
542
|
-
// would evict the carried page before injection. Snapshotting the carry
|
|
543
|
-
// BEFORE recording this turn keeps the earlier page in the injection.
|
|
544
|
-
const workingSet = new WorkingSet(1);
|
|
545
|
-
|
|
546
|
-
providerStub = selectProvider(["topic-a"]); // turn 1 → topic-a
|
|
547
|
-
await orchestrate(makeTurn(1, "apple"), {
|
|
548
|
-
sectionIndex: lanes.sectionIndex,
|
|
549
|
-
needle: lanes.needle,
|
|
550
|
-
denseConfig: config,
|
|
551
|
-
edgeGraph: lanes.edgeGraph,
|
|
552
|
-
workingSet,
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
denseHits = [{ article: "topic-b", section: 0 }];
|
|
556
|
-
providerStub = selectProvider(["topic-b"]); // turn 2 → topic-b
|
|
557
|
-
const t2 = await orchestrate(makeTurn(2, "cherry"), {
|
|
558
|
-
sectionIndex: lanes.sectionIndex,
|
|
559
|
-
needle: lanes.needle,
|
|
560
|
-
denseConfig: config,
|
|
561
|
-
edgeGraph: lanes.edgeGraph,
|
|
562
|
-
workingSet,
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
expect(t2.currentSelections.map((s) => s.slug)).toEqual(["topic-b"]);
|
|
566
|
-
expect(t2.finalInjection).toContain("topic-a"); // carried despite the cap
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test("a stale non-pinned page ages out of the carry-forward window", async () => {
|
|
570
|
-
const lanes = await buildLanes();
|
|
571
|
-
// window 2: a non-pinned entry unseen for >2 turns evicts.
|
|
572
|
-
const workingSet = new WorkingSet(150, 2);
|
|
573
|
-
|
|
574
|
-
providerStub = selectProvider(["topic-a"]); // turn 1 selects topic-a
|
|
575
|
-
const t1 = await orchestrate(makeTurn(1, "apple"), {
|
|
576
|
-
sectionIndex: lanes.sectionIndex,
|
|
577
|
-
needle: lanes.needle,
|
|
578
|
-
denseConfig: config,
|
|
579
|
-
edgeGraph: lanes.edgeGraph,
|
|
580
|
-
workingSet,
|
|
581
|
-
});
|
|
582
|
-
expect(t1.finalInjection).toContain("topic-a");
|
|
583
|
-
|
|
584
|
-
// Turns 2–4 never re-select topic-a. By turn 4 (4-1=3 > 2) it ages out.
|
|
585
|
-
providerStub = selectProvider([]);
|
|
586
|
-
await orchestrate(makeTurn(2, "x"), {
|
|
587
|
-
sectionIndex: lanes.sectionIndex,
|
|
588
|
-
needle: lanes.needle,
|
|
589
|
-
denseConfig: config,
|
|
590
|
-
edgeGraph: lanes.edgeGraph,
|
|
591
|
-
workingSet,
|
|
592
|
-
});
|
|
593
|
-
await orchestrate(makeTurn(3, "x"), {
|
|
594
|
-
sectionIndex: lanes.sectionIndex,
|
|
595
|
-
needle: lanes.needle,
|
|
596
|
-
denseConfig: config,
|
|
597
|
-
edgeGraph: lanes.edgeGraph,
|
|
598
|
-
workingSet,
|
|
599
|
-
});
|
|
600
|
-
const t4 = await orchestrate(makeTurn(4, "x"), {
|
|
601
|
-
sectionIndex: lanes.sectionIndex,
|
|
602
|
-
needle: lanes.needle,
|
|
603
|
-
denseConfig: config,
|
|
604
|
-
edgeGraph: lanes.edgeGraph,
|
|
605
|
-
workingSet,
|
|
606
|
-
});
|
|
607
|
-
expect(t4.finalInjection).not.toContain("topic-a");
|
|
608
|
-
});
|
|
642
|
+
const result = await orchestrate(makeTurn(1, "apple"), depsOf(lanes));
|
|
609
643
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
providerStub = selectProvider(["topic-a"], ["topic-a"]);
|
|
614
|
-
await orchestrate(makeTurn(1, "apple"), {
|
|
615
|
-
sectionIndex: lanes.sectionIndex,
|
|
616
|
-
needle: lanes.needle,
|
|
617
|
-
denseConfig: config,
|
|
618
|
-
edgeGraph: lanes.edgeGraph,
|
|
619
|
-
workingSet: ws,
|
|
620
|
-
});
|
|
621
|
-
expect(ws.union().has("topic-a")).toBe(true);
|
|
644
|
+
const entries = result.lanes.finder.filter((c) => c.slug === "topic-a");
|
|
645
|
+
expect(entries).toHaveLength(1);
|
|
646
|
+
expect(entries[0]!.lane).toBe("needle");
|
|
622
647
|
});
|
|
623
648
|
});
|
|
624
649
|
|
|
625
650
|
// ---------------------------------------------------------------------------
|
|
626
|
-
// Degradation: an empty pool and
|
|
651
|
+
// Degradation: an empty pool and the recall-safe omitted-ids path.
|
|
627
652
|
// ---------------------------------------------------------------------------
|
|
628
653
|
|
|
629
654
|
describe("orchestrate — degradation", () => {
|
|
630
|
-
test("an empty pool yields no selections
|
|
655
|
+
test("an empty pool yields no selections", async () => {
|
|
631
656
|
const lanes = await buildLanes();
|
|
632
657
|
providerStub = selectProvider([]);
|
|
633
|
-
const result = await orchestrate(
|
|
634
|
-
|
|
635
|
-
needle: { query: () => [], bestSection: () => -1 },
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
});
|
|
640
|
-
expect(result.currentSelections).toEqual([]);
|
|
641
|
-
expect(result.finalInjection).toEqual([]);
|
|
658
|
+
const result = await orchestrate(
|
|
659
|
+
makeTurn(1, "zzzzz no-match"),
|
|
660
|
+
depsOf(lanes, { needle: { query: () => [], bestSection: () => -1 } }),
|
|
661
|
+
);
|
|
662
|
+
expect(result.selections).toEqual([]);
|
|
663
|
+
expect(result.lanes.finder).toEqual([]);
|
|
642
664
|
});
|
|
643
665
|
|
|
644
666
|
test("omitted ids keeps ALL pooled candidates (recall-safe)", async () => {
|
|
@@ -648,15 +670,12 @@ describe("orchestrate — degradation", () => {
|
|
|
648
670
|
name: "stub",
|
|
649
671
|
sendMessage: async () => toolUseResponse({}), // omitted ids → keep all
|
|
650
672
|
};
|
|
651
|
-
const result = await orchestrate(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
});
|
|
658
|
-
expect(new Set(result.finalInjection)).toEqual(
|
|
659
|
-
new Set(["topic-a", "topic-b", "topic-d"]),
|
|
673
|
+
const result = await orchestrate(
|
|
674
|
+
makeTurn(1, "apple"),
|
|
675
|
+
depsOf(lanes, { coreSlugs: ["topic-c"] }),
|
|
676
|
+
);
|
|
677
|
+
expect(new Set(result.selections.map((s) => s.slug))).toEqual(
|
|
678
|
+
new Set(["topic-c", "topic-a", "topic-b", "topic-d"]),
|
|
660
679
|
);
|
|
661
680
|
});
|
|
662
681
|
});
|