@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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation everInjected record for memory-v3's frozen-card carry.
|
|
3
|
+
*
|
|
4
|
+
* Backed by `memory_v3_ever_injected` (migration 277): one row per
|
|
5
|
+
* (conversation, page-slug) the v3 injector ever attached as a card. The
|
|
6
|
+
* active (non-pruned) slug set is the injection dedup record — a slug present
|
|
7
|
+
* here rides the cached message prefix and must not be re-rendered — and
|
|
8
|
+
* `bytes` sums into the resident footprint the prune valve bounds. Rows are
|
|
9
|
+
* never deleted by pruning (`pruned_at` is set instead) so the record stays
|
|
10
|
+
* auditable; a pruned page that is re-selected re-injects by clearing
|
|
11
|
+
* `pruned_at` on upsert. `clearConversation` is the compaction reset: the
|
|
12
|
+
* cached blocks those slugs lived on are gone, so future turns are free to
|
|
13
|
+
* re-inject them.
|
|
14
|
+
*
|
|
15
|
+
* Fork semantics mirror v2's activation-store hooks (both run synchronously
|
|
16
|
+
* inside the `forkConversation()` transaction — see
|
|
17
|
+
* `assistant/src/memory/conversation-crud.ts`):
|
|
18
|
+
* - full-history forks copy the parent's rows wholesale
|
|
19
|
+
* (`forkEverInjected`), pruned state included;
|
|
20
|
+
* - truncated forks seed from the slugs scanned out of the inherited
|
|
21
|
+
* messages' persisted card blocks (`seedEverInjectedFromSlugs`) — a
|
|
22
|
+
* wholesale copy would over-claim slugs injected on turns the child does
|
|
23
|
+
* not contain, suppressing their re-injection forever.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
type DrizzleDb,
|
|
28
|
+
getDb,
|
|
29
|
+
getSqliteFrom,
|
|
30
|
+
} from "../../../memory/db-connection.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Message-metadata key the v3 injector persists each turn's card block under
|
|
34
|
+
* (the v3 counterpart of v2's `memoryInjectedBlock`). Shared by the writer,
|
|
35
|
+
* the conversation-load rehydration splice, and the truncated-fork seed scan
|
|
36
|
+
* in `conversation-crud.ts` so all three agree on the key.
|
|
37
|
+
*/
|
|
38
|
+
export const MEMORY_V3_INJECTED_BLOCK_METADATA_KEY = "memoryV3InjectedBlock";
|
|
39
|
+
|
|
40
|
+
export interface EverInjectedEntry {
|
|
41
|
+
bytes: number;
|
|
42
|
+
/** Epoch ms the prune valve removed the card, or `null` while resident. */
|
|
43
|
+
prunedAt: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The full per-conversation record, pruned rows included. Test oracle only —
|
|
48
|
+
* no production code path reads it; production consumers use the narrower
|
|
49
|
+
* accessors ({@link getActiveSlugs}, {@link getActiveEntries},
|
|
50
|
+
* {@link getPrunedSlugs}, {@link residentBytes}).
|
|
51
|
+
*/
|
|
52
|
+
export function getInjected(
|
|
53
|
+
conversationId: string,
|
|
54
|
+
): Map<string, EverInjectedEntry> {
|
|
55
|
+
const rows = getSqliteFrom(getDb())
|
|
56
|
+
.query(
|
|
57
|
+
/*sql*/ `
|
|
58
|
+
SELECT slug, bytes, pruned_at AS prunedAt FROM memory_v3_ever_injected
|
|
59
|
+
WHERE conversation_id = ?
|
|
60
|
+
`,
|
|
61
|
+
)
|
|
62
|
+
.all(conversationId) as Array<{
|
|
63
|
+
slug: string;
|
|
64
|
+
bytes: number;
|
|
65
|
+
prunedAt: number | null;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
return new Map(
|
|
69
|
+
rows.map((row) => [row.slug, { bytes: row.bytes, prunedAt: row.prunedAt }]),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The injection dedup set: slugs whose cards are currently resident. */
|
|
74
|
+
export function getActiveSlugs(conversationId: string): Set<string> {
|
|
75
|
+
const rows = getSqliteFrom(getDb())
|
|
76
|
+
.query(
|
|
77
|
+
/*sql*/ `
|
|
78
|
+
SELECT slug FROM memory_v3_ever_injected
|
|
79
|
+
WHERE conversation_id = ? AND pruned_at IS NULL
|
|
80
|
+
`,
|
|
81
|
+
)
|
|
82
|
+
.all(conversationId) as Array<{ slug: string }>;
|
|
83
|
+
return new Set(rows.map((row) => row.slug));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One active (resident) row of the prune valve's candidate set. */
|
|
87
|
+
export interface ActiveInjectedEntry {
|
|
88
|
+
slug: string;
|
|
89
|
+
bytes: number;
|
|
90
|
+
/** Epoch ms the card was (last) injected — the recency fallback for slugs
|
|
91
|
+
* with no selection rows (e.g. rows copied by a full fork). */
|
|
92
|
+
injectedAt: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Active (non-pruned) rows with byte and injection-time accounting — the
|
|
97
|
+
* prune valve's candidate set ({@link ActiveInjectedEntry}).
|
|
98
|
+
*/
|
|
99
|
+
export function getActiveEntries(
|
|
100
|
+
conversationId: string,
|
|
101
|
+
): ActiveInjectedEntry[] {
|
|
102
|
+
return getSqliteFrom(getDb())
|
|
103
|
+
.query(
|
|
104
|
+
/*sql*/ `
|
|
105
|
+
SELECT slug, bytes, injected_at AS injectedAt FROM memory_v3_ever_injected
|
|
106
|
+
WHERE conversation_id = ? AND pruned_at IS NULL
|
|
107
|
+
`,
|
|
108
|
+
)
|
|
109
|
+
.all(conversationId) as ActiveInjectedEntry[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Slugs currently marked pruned — the card-section skip set shared by the
|
|
114
|
+
* live-history strip and the `loadFromDb` rehydration filter (see
|
|
115
|
+
* `prune.ts` / `daemon/conversation.ts`).
|
|
116
|
+
*/
|
|
117
|
+
export function getPrunedSlugs(conversationId: string): Set<string> {
|
|
118
|
+
const rows = getSqliteFrom(getDb())
|
|
119
|
+
.query(
|
|
120
|
+
/*sql*/ `
|
|
121
|
+
SELECT slug FROM memory_v3_ever_injected
|
|
122
|
+
WHERE conversation_id = ? AND pruned_at IS NOT NULL
|
|
123
|
+
`,
|
|
124
|
+
)
|
|
125
|
+
.all(conversationId) as Array<{ slug: string }>;
|
|
126
|
+
return new Set(rows.map((row) => row.slug));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Upsert this turn's injected cards. Re-recording an existing slug clears
|
|
131
|
+
* `pruned_at` and refreshes `bytes`/`injected_at` — a pruned page that is
|
|
132
|
+
* re-selected re-injects as a fresh card.
|
|
133
|
+
*/
|
|
134
|
+
export function recordInjected(
|
|
135
|
+
conversationId: string,
|
|
136
|
+
entries: Array<{ slug: string; bytes: number }>,
|
|
137
|
+
at: number = Date.now(),
|
|
138
|
+
): void {
|
|
139
|
+
if (entries.length === 0) return;
|
|
140
|
+
const stmt = getSqliteFrom(getDb()).query(/*sql*/ `
|
|
141
|
+
INSERT INTO memory_v3_ever_injected (conversation_id, slug, injected_at, bytes, pruned_at)
|
|
142
|
+
VALUES (?, ?, ?, ?, NULL)
|
|
143
|
+
ON CONFLICT (conversation_id, slug) DO UPDATE SET
|
|
144
|
+
injected_at = excluded.injected_at,
|
|
145
|
+
bytes = excluded.bytes,
|
|
146
|
+
pruned_at = NULL
|
|
147
|
+
`);
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
stmt.run(conversationId, entry.slug, at, entry.bytes);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Mark cards pruned from the live context. Rows are never deleted — the
|
|
155
|
+
* record stays auditable and the slugs stay eligible for re-injection.
|
|
156
|
+
*/
|
|
157
|
+
export function markPruned(
|
|
158
|
+
conversationId: string,
|
|
159
|
+
slugs: string[],
|
|
160
|
+
at: number,
|
|
161
|
+
): void {
|
|
162
|
+
if (slugs.length === 0) return;
|
|
163
|
+
const placeholders = slugs.map(() => "?").join(", ");
|
|
164
|
+
getSqliteFrom(getDb())
|
|
165
|
+
.query(
|
|
166
|
+
/*sql*/ `
|
|
167
|
+
UPDATE memory_v3_ever_injected SET pruned_at = ?
|
|
168
|
+
WHERE conversation_id = ? AND slug IN (${placeholders})
|
|
169
|
+
`,
|
|
170
|
+
)
|
|
171
|
+
.run(at, conversationId, ...slugs);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Delete the conversation's entire record. Compaction reset: the cached
|
|
176
|
+
* blocks are gone from history, so every slug must become re-injectable.
|
|
177
|
+
*/
|
|
178
|
+
export function clearConversation(conversationId: string): void {
|
|
179
|
+
getSqliteFrom(getDb())
|
|
180
|
+
.query(
|
|
181
|
+
/*sql*/ `
|
|
182
|
+
DELETE FROM memory_v3_ever_injected WHERE conversation_id = ?
|
|
183
|
+
`,
|
|
184
|
+
)
|
|
185
|
+
.run(conversationId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Total bytes of resident (non-pruned) cards — the prune-valve input. */
|
|
189
|
+
export function residentBytes(conversationId: string): number {
|
|
190
|
+
const row = getSqliteFrom(getDb())
|
|
191
|
+
.query(
|
|
192
|
+
/*sql*/ `
|
|
193
|
+
SELECT COALESCE(SUM(bytes), 0) AS total FROM memory_v3_ever_injected
|
|
194
|
+
WHERE conversation_id = ? AND pruned_at IS NULL
|
|
195
|
+
`,
|
|
196
|
+
)
|
|
197
|
+
.get(conversationId) as { total: number };
|
|
198
|
+
return row.total;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Copy the parent conversation's rows to a new conversation id, pruned state
|
|
203
|
+
* included. No-op if the parent has no rows. Mirrors `forkActivationState`:
|
|
204
|
+
* synchronous so it can run inside the bun:sqlite transaction that wraps
|
|
205
|
+
* `forkConversation()`, keeping the copy atomic with the message copies.
|
|
206
|
+
* Full-history forks only — truncated forks must use
|
|
207
|
+
* `seedEverInjectedFromSlugs` instead.
|
|
208
|
+
*/
|
|
209
|
+
export function forkEverInjected(
|
|
210
|
+
db: DrizzleDb,
|
|
211
|
+
parentConversationId: string,
|
|
212
|
+
newConversationId: string,
|
|
213
|
+
): void {
|
|
214
|
+
getSqliteFrom(db)
|
|
215
|
+
.query(
|
|
216
|
+
/*sql*/ `
|
|
217
|
+
INSERT INTO memory_v3_ever_injected (conversation_id, slug, injected_at, bytes, pruned_at)
|
|
218
|
+
SELECT ?, slug, injected_at, bytes, pruned_at FROM memory_v3_ever_injected
|
|
219
|
+
WHERE conversation_id = ?
|
|
220
|
+
ON CONFLICT (conversation_id, slug) DO UPDATE SET
|
|
221
|
+
injected_at = excluded.injected_at,
|
|
222
|
+
bytes = excluded.bytes,
|
|
223
|
+
pruned_at = excluded.pruned_at
|
|
224
|
+
`,
|
|
225
|
+
)
|
|
226
|
+
.run(newConversationId, parentConversationId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Seed a truncated fork's record from the slugs whose card blocks the child
|
|
231
|
+
* actually inherited (scanned out of the copied messages'
|
|
232
|
+
* `MEMORY_V3_INJECTED_BLOCK_METADATA_KEY` metadata). Mirrors
|
|
233
|
+
* `seedForkActivationState`: a wholesale copy would over-claim, while seeding
|
|
234
|
+
* nothing would re-attach every inherited card as a duplicate.
|
|
235
|
+
*
|
|
236
|
+
* Rows are stamped `injected_at = at` and `bytes = 0` — the rendered size of
|
|
237
|
+
* inherited cards is unknown, so `residentBytes` accounting restarts from the
|
|
238
|
+
* fork's own injections.
|
|
239
|
+
*
|
|
240
|
+
* The parent's `pruned_at` tombstones are carried over: pruning leaves the
|
|
241
|
+
* persisted metadata block intact and relies on the tombstone to filter the
|
|
242
|
+
* section out at rehydration, so the metadata scan feeding this seed
|
|
243
|
+
* necessarily sees pruned cards' sections too. Seeding those slugs as active
|
|
244
|
+
* would resurrect parent-pruned cards in the child on its next load (and
|
|
245
|
+
* diverge from the full-fork copy path, which preserves tombstones). A
|
|
246
|
+
* tombstoned seed keeps the child's rehydrated view identical to the parent's
|
|
247
|
+
* live view at fork time; re-selection clears the tombstone and re-injects,
|
|
248
|
+
* same as in the parent.
|
|
249
|
+
*
|
|
250
|
+
* No-op when the child inherited no card blocks. Synchronous so it can run
|
|
251
|
+
* inside the `forkConversation()` transaction.
|
|
252
|
+
*/
|
|
253
|
+
export function seedEverInjectedFromSlugs(
|
|
254
|
+
db: DrizzleDb,
|
|
255
|
+
parentConversationId: string,
|
|
256
|
+
newConversationId: string,
|
|
257
|
+
slugs: string[],
|
|
258
|
+
at: number,
|
|
259
|
+
): void {
|
|
260
|
+
if (slugs.length === 0) return;
|
|
261
|
+
const prunedRows = getSqliteFrom(db)
|
|
262
|
+
.query(
|
|
263
|
+
/*sql*/ `
|
|
264
|
+
SELECT slug, pruned_at AS prunedAt FROM memory_v3_ever_injected
|
|
265
|
+
WHERE conversation_id = ? AND pruned_at IS NOT NULL
|
|
266
|
+
`,
|
|
267
|
+
)
|
|
268
|
+
.all(parentConversationId) as Array<{ slug: string; prunedAt: number }>;
|
|
269
|
+
const parentPrunedAt = new Map(prunedRows.map((r) => [r.slug, r.prunedAt]));
|
|
270
|
+
const stmt = getSqliteFrom(db).query(/*sql*/ `
|
|
271
|
+
INSERT INTO memory_v3_ever_injected (conversation_id, slug, injected_at, bytes, pruned_at)
|
|
272
|
+
VALUES (?, ?, ?, 0, ?)
|
|
273
|
+
ON CONFLICT (conversation_id, slug) DO NOTHING
|
|
274
|
+
`);
|
|
275
|
+
for (const slug of slugs) {
|
|
276
|
+
stmt.run(newConversationId, slug, at, parentPrunedAt.get(slug) ?? null);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the frecency hot-set lane (`hot-set.ts`) and its config schema.
|
|
3
|
+
*
|
|
4
|
+
* `computeHotSet` takes the db handle directly (no module mocks needed):
|
|
5
|
+
* each test seeds an in-memory SQLite db with `memory_v3_selections` rows
|
|
6
|
+
* (via the real migration) and asserts the decayed-frequency ranking.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Database } from "bun:sqlite";
|
|
10
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
13
|
+
|
|
14
|
+
import { MemoryV3ConfigSchema } from "../../../config/schemas/memory-v3.js";
|
|
15
|
+
import { migrateAddMemoryV3Selections } from "../../../memory/migrations/268-add-memory-v3-selections.js";
|
|
16
|
+
import * as schema from "../../../memory/schema.js";
|
|
17
|
+
import { computeHotSet, type HotSetOptions } from "./hot-set.js";
|
|
18
|
+
|
|
19
|
+
const HALF_LIFE_MS = 1000;
|
|
20
|
+
const NOW = 100_000;
|
|
21
|
+
|
|
22
|
+
let sqlite: Database;
|
|
23
|
+
let db: ReturnType<typeof drizzle<typeof schema>>;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
sqlite = new Database(":memory:");
|
|
27
|
+
db = drizzle(sqlite, { schema });
|
|
28
|
+
migrateAddMemoryV3Selections(db);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let nextTurn = 0;
|
|
32
|
+
/** Insert one selection row per createdAt (distinct turns keep the PK unique). */
|
|
33
|
+
function seed(slug: string, createdAts: number[]): void {
|
|
34
|
+
const stmt = sqlite.query(
|
|
35
|
+
`INSERT INTO memory_v3_selections (conversation_id, turn, slug, source, pinned, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
36
|
+
);
|
|
37
|
+
for (const createdAt of createdAts) {
|
|
38
|
+
stmt.run("conv-1", nextTurn++, slug, "needle", 0, createdAt);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hotSet(overrides: Partial<HotSetOptions> = {}) {
|
|
43
|
+
return computeHotSet(
|
|
44
|
+
{ db },
|
|
45
|
+
{
|
|
46
|
+
k: 10,
|
|
47
|
+
halfLifeMs: HALF_LIFE_MS,
|
|
48
|
+
now: NOW,
|
|
49
|
+
excludeSlugs: new Set<string>(),
|
|
50
|
+
...overrides,
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("computeHotSet", () => {
|
|
56
|
+
test("empty table yields empty result", () => {
|
|
57
|
+
expect(hotSet()).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("newer selections outweigh older ones at the same count", () => {
|
|
61
|
+
seed("page-old", [NOW - 10 * HALF_LIFE_MS, NOW - 10 * HALF_LIFE_MS]);
|
|
62
|
+
seed("page-new", [NOW, NOW]);
|
|
63
|
+
const result = hotSet();
|
|
64
|
+
expect(result.map((e) => e.slug)).toEqual(["page-new", "page-old"]);
|
|
65
|
+
expect(result[0]!.score).toBeGreaterThan(result[1]!.score);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("decay halves at exactly one half-life", () => {
|
|
69
|
+
seed("page-a", [NOW]);
|
|
70
|
+
seed("page-b", [NOW - HALF_LIFE_MS]);
|
|
71
|
+
const result = hotSet();
|
|
72
|
+
const bySlug = new Map(result.map((e) => [e.slug, e.score]));
|
|
73
|
+
expect(bySlug.get("page-a")).toBeCloseTo(1, 10);
|
|
74
|
+
expect(bySlug.get("page-b")).toBeCloseTo(0.5, 10);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("scores sum across a slug's selections", () => {
|
|
78
|
+
seed("page-a", [NOW, NOW - HALF_LIFE_MS]);
|
|
79
|
+
expect(hotSet()[0]!.score).toBeCloseTo(1.5, 10);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("k cut keeps only the top-k", () => {
|
|
83
|
+
seed("page-a", [NOW, NOW, NOW]);
|
|
84
|
+
seed("page-b", [NOW, NOW]);
|
|
85
|
+
seed("page-c", [NOW]);
|
|
86
|
+
expect(hotSet({ k: 2 }).map((e) => e.slug)).toEqual(["page-a", "page-b"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("excludeSlugs filters before the k cut", () => {
|
|
90
|
+
seed("page-a", [NOW, NOW, NOW]);
|
|
91
|
+
seed("page-b", [NOW, NOW]);
|
|
92
|
+
seed("page-c", [NOW]);
|
|
93
|
+
const result = hotSet({ k: 2, excludeSlugs: new Set(["page-a"]) });
|
|
94
|
+
expect(result.map((e) => e.slug)).toEqual(["page-b", "page-c"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("ties break deterministically by slug ascending", () => {
|
|
98
|
+
seed("page-b", [NOW]);
|
|
99
|
+
seed("page-a", [NOW]);
|
|
100
|
+
seed("page-c", [NOW]);
|
|
101
|
+
expect(hotSet().map((e) => e.slug)).toEqual(["page-a", "page-b", "page-c"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("deterministic for fixed inputs", () => {
|
|
105
|
+
seed("page-a", [NOW, NOW - 3 * HALF_LIFE_MS]);
|
|
106
|
+
seed("page-b", [NOW - HALF_LIFE_MS]);
|
|
107
|
+
expect(hotSet()).toEqual(hotSet());
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("future-dated rows are clamped to weight 1", () => {
|
|
111
|
+
seed("page-a", [NOW + 50 * HALF_LIFE_MS]);
|
|
112
|
+
expect(hotSet()[0]!.score).toBeCloseTo(1, 10);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("MemoryV3ConfigSchema hotSet", () => {
|
|
117
|
+
test("defaults apply when omitted", () => {
|
|
118
|
+
const config = MemoryV3ConfigSchema.parse({});
|
|
119
|
+
expect(config.hotSet).toEqual({ k: 40, halfLifeDays: 14 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("explicit values parse", () => {
|
|
123
|
+
const config = MemoryV3ConfigSchema.parse({
|
|
124
|
+
hotSet: { k: 5, halfLifeDays: 7 },
|
|
125
|
+
});
|
|
126
|
+
expect(config.hotSet).toEqual({ k: 5, halfLifeDays: 7 });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("rejects negative k", () => {
|
|
130
|
+
expect(() => MemoryV3ConfigSchema.parse({ hotSet: { k: -1 } })).toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("rejects negative halfLifeDays", () => {
|
|
134
|
+
expect(() =>
|
|
135
|
+
MemoryV3ConfigSchema.parse({ hotSet: { halfLifeDays: -1 } }),
|
|
136
|
+
).toThrow();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frecency hot-set lane.
|
|
3
|
+
*
|
|
4
|
+
* Scores every page slug in `memory_v3_selections` (all conversations) by its
|
|
5
|
+
* exponentially-decayed selection count and returns the top-K. The hot set is
|
|
6
|
+
* the lane that keeps *seasonally recurrent* pages in the candidate pool even
|
|
7
|
+
* when the current message gives the finder lanes nothing to match on; the
|
|
8
|
+
* curated core set (a separate lane) covers the associative-texture class
|
|
9
|
+
* frecency cannot discriminate.
|
|
10
|
+
*
|
|
11
|
+
* Each selection row contributes `2^(−age / halfLifeMs)` to its slug's score,
|
|
12
|
+
* so a selection exactly one half-life old counts half as much as one made
|
|
13
|
+
* now. Slugs in `excludeSlugs` (the core set) are dropped before the K cut —
|
|
14
|
+
* the hot lane never duplicates core.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type DrizzleDb,
|
|
19
|
+
getSqliteFrom,
|
|
20
|
+
} from "../../../memory/db-connection.js";
|
|
21
|
+
import type { Slug } from "./types.js";
|
|
22
|
+
|
|
23
|
+
export interface HotSetDeps {
|
|
24
|
+
/** Handle to the database containing `memory_v3_selections`. */
|
|
25
|
+
db: DrizzleDb;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HotSetOptions {
|
|
29
|
+
/** Maximum number of slugs returned. */
|
|
30
|
+
k: number;
|
|
31
|
+
/** Decay half-life in milliseconds: a selection this old scores 0.5. */
|
|
32
|
+
halfLifeMs: number;
|
|
33
|
+
/** Reference timestamp (ms epoch) ages are measured against. */
|
|
34
|
+
now: number;
|
|
35
|
+
/** Slugs excluded from the result (the core set — hot never duplicates core). */
|
|
36
|
+
excludeSlugs: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface HotSetEntry {
|
|
40
|
+
slug: Slug;
|
|
41
|
+
score: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SelectionAgeRow {
|
|
45
|
+
slug: string;
|
|
46
|
+
created_at: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compute the top-`k` hot slugs by exponentially-decayed selection frequency.
|
|
51
|
+
*
|
|
52
|
+
* Deterministic for fixed inputs: ties break score desc, then slug asc. The
|
|
53
|
+
* decay sum runs in TS over `(slug, created_at)` pairs — the selections table
|
|
54
|
+
* is small enough that reading it directly beats pushing the math into SQL.
|
|
55
|
+
*/
|
|
56
|
+
export function computeHotSet(
|
|
57
|
+
deps: HotSetDeps,
|
|
58
|
+
opts: HotSetOptions,
|
|
59
|
+
): HotSetEntry[] {
|
|
60
|
+
const { k, halfLifeMs, now, excludeSlugs } = opts;
|
|
61
|
+
if (k <= 0) return [];
|
|
62
|
+
|
|
63
|
+
const rows = getSqliteFrom(deps.db)
|
|
64
|
+
.query(
|
|
65
|
+
/*sql*/ `
|
|
66
|
+
SELECT slug, created_at FROM memory_v3_selections
|
|
67
|
+
`,
|
|
68
|
+
)
|
|
69
|
+
.all() as SelectionAgeRow[];
|
|
70
|
+
|
|
71
|
+
const scores = new Map<Slug, number>();
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
if (excludeSlugs.has(row.slug)) continue;
|
|
74
|
+
// Clamp future-dated rows (clock skew) to "now" rather than letting them
|
|
75
|
+
// score above 1 per selection.
|
|
76
|
+
const age = Math.max(0, now - row.created_at);
|
|
77
|
+
const weight = 2 ** (-age / halfLifeMs);
|
|
78
|
+
scores.set(row.slug, (scores.get(row.slug) ?? 0) + weight);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [...scores.entries()]
|
|
82
|
+
.map(([slug, score]) => ({ slug, score }))
|
|
83
|
+
.sort((a, b) => b.score - a.score || (a.slug < b.slug ? -1 : 1))
|
|
84
|
+
.slice(0, k);
|
|
85
|
+
}
|