@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,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the memory-v3 injection layer (`injector.ts`): frozen net-new
|
|
3
|
+
* cards + ephemeral spotlight.
|
|
4
|
+
*
|
|
5
|
+
* - net-new dedup: a turn re-selecting already-injected pages renders zero
|
|
6
|
+
* new cards (empty-text block — still produced, so v2 suppression holds);
|
|
7
|
+
* - commit deferral: the everInjected-store write happens in the block's
|
|
8
|
+
* attachment-commit callback (invoked by assembly on user-tail turns),
|
|
9
|
+
* never in `produce()` itself;
|
|
10
|
+
* - trust gate: an untrusted remote actor's turn produces nothing and
|
|
11
|
+
* records nothing (the v2 personal-memory gate);
|
|
12
|
+
* - fork dedup: a conversation whose everInjected record was seeded from
|
|
13
|
+
* inherited blocks does not re-render those slugs;
|
|
14
|
+
* - prune round-trip: a pruned slug that is re-selected re-injects;
|
|
15
|
+
* - shadow mode: attaches nothing and records nothing, but logs the
|
|
16
|
+
* would-inject set;
|
|
17
|
+
* - spotlight: current-window entries only, re-rendered (never accumulated),
|
|
18
|
+
* bounded by `n × (windowTurns + 1)`, live-only, absent from the
|
|
19
|
+
* persistent card layer.
|
|
20
|
+
*
|
|
21
|
+
* Orchestration is stubbed at the `observeTurn` seam (the injectors' shared
|
|
22
|
+
* input); the everInjected store runs REAL against an in-memory SQLite DB so
|
|
23
|
+
* the dedup contract is exercised end-to-end. `mock.module` is process-global,
|
|
24
|
+
* so every stub delegates to the real implementation unless this file's tests
|
|
25
|
+
* are running (`injectionMockActive`) — mirrors the sibling test files.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Database } from "bun:sqlite";
|
|
29
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
30
|
+
|
|
31
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
32
|
+
|
|
33
|
+
import { unwrapMemoryBlock } from "../../../../memory/memory-marker.js";
|
|
34
|
+
import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
|
|
35
|
+
import { migrateAddMemoryV3EverInjected } from "../../../../memory/migrations/277-add-memory-v3-ever-injected.js";
|
|
36
|
+
import * as schema from "../../../../memory/schema.js";
|
|
37
|
+
import type { InjectionBlock } from "../../../types.js";
|
|
38
|
+
import type { OrchestrateResult } from "../orchestrate.js";
|
|
39
|
+
import {
|
|
40
|
+
MEMORY_V3_COMMIT_META_KEY,
|
|
41
|
+
type Section,
|
|
42
|
+
type Slug,
|
|
43
|
+
} from "../types.js";
|
|
44
|
+
|
|
45
|
+
const realConfigLoader = { ...(await import("../../../../config/loader.js")) };
|
|
46
|
+
const realFlags = {
|
|
47
|
+
...(await import("../../../../config/assistant-feature-flags.js")),
|
|
48
|
+
};
|
|
49
|
+
const realDbConnection = {
|
|
50
|
+
...(await import("../../../../memory/db-connection.js")),
|
|
51
|
+
};
|
|
52
|
+
const realPageContent = { ...(await import("../page-content.js")) };
|
|
53
|
+
const realShadowPlugin = { ...(await import("../shadow-plugin.js")) };
|
|
54
|
+
|
|
55
|
+
let injectionMockActive = false;
|
|
56
|
+
|
|
57
|
+
// ─── mutable test state ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
let liveEnabled = false;
|
|
60
|
+
let shadowEnabled = false;
|
|
61
|
+
let memoryEnabled = true;
|
|
62
|
+
let spotlightConfig = { n: 6, windowTurns: 2 };
|
|
63
|
+
/** `null` disables the prune valve (the default for tests not exercising it —
|
|
64
|
+
* `runPruneValve` bails when the config block is absent). */
|
|
65
|
+
let pruneConfig: {
|
|
66
|
+
maxResidentBytes: number;
|
|
67
|
+
targetResidentBytes: number;
|
|
68
|
+
} | null = null;
|
|
69
|
+
/** Canned orchestrate result per turnIndex; `null` simulates a failed turn. */
|
|
70
|
+
let turnResults = new Map<number, OrchestrateResult | null>();
|
|
71
|
+
const observeTurnSpy = mock(
|
|
72
|
+
async (
|
|
73
|
+
_conversationId: string,
|
|
74
|
+
turnIndex: number,
|
|
75
|
+
): Promise<OrchestrateResult | null> => turnResults.get(turnIndex) ?? null,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const logCalls: Array<{ data: unknown; msg: string }> = [];
|
|
79
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
80
|
+
getLogger: () => ({
|
|
81
|
+
info: (data: unknown, msg: string) => logCalls.push({ data, msg }),
|
|
82
|
+
warn: (data: unknown, msg: string) => logCalls.push({ data, msg }),
|
|
83
|
+
error: () => {},
|
|
84
|
+
debug: () => {},
|
|
85
|
+
}),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
let testSqlite: Database;
|
|
89
|
+
let testDb = makeDb();
|
|
90
|
+
function makeDb() {
|
|
91
|
+
testSqlite = new Database(":memory:");
|
|
92
|
+
const db = drizzle(testSqlite, { schema });
|
|
93
|
+
migrateAddMemoryV3EverInjected(db);
|
|
94
|
+
// The prune valve's recency ranking reads `memory_v3_selections`.
|
|
95
|
+
migrateAddMemoryV3Selections(db);
|
|
96
|
+
// The prune valve plans only against slugs whose card sections are
|
|
97
|
+
// locatable in persisted `memoryV3InjectedBlock` rows
|
|
98
|
+
// (`collectPersistedV3Cards`) — minimal `messages` shape it reads.
|
|
99
|
+
testSqlite.run(/*sql*/ `
|
|
100
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
conversation_id TEXT NOT NULL,
|
|
103
|
+
role TEXT NOT NULL,
|
|
104
|
+
content TEXT NOT NULL,
|
|
105
|
+
metadata TEXT,
|
|
106
|
+
created_at INTEGER NOT NULL
|
|
107
|
+
)
|
|
108
|
+
`);
|
|
109
|
+
return db;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
mock.module("../../../../memory/db-connection.js", () => ({
|
|
113
|
+
...realDbConnection,
|
|
114
|
+
getDb: () => (injectionMockActive ? testDb : realDbConnection.getDb()),
|
|
115
|
+
getSqliteFrom: (db: unknown) =>
|
|
116
|
+
injectionMockActive
|
|
117
|
+
? testSqlite
|
|
118
|
+
: realDbConnection.getSqliteFrom(
|
|
119
|
+
db as Parameters<typeof realDbConnection.getSqliteFrom>[0],
|
|
120
|
+
),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
mock.module("../../../../config/loader.js", () => ({
|
|
124
|
+
...realConfigLoader,
|
|
125
|
+
getConfig: () =>
|
|
126
|
+
injectionMockActive
|
|
127
|
+
? {
|
|
128
|
+
memory: {
|
|
129
|
+
enabled: memoryEnabled,
|
|
130
|
+
v3: {
|
|
131
|
+
spotlight: spotlightConfig,
|
|
132
|
+
prune: pruneConfig ?? undefined,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
: realConfigLoader.getConfig(),
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
// The prune valve resolves the live conversation through the daemon registry
|
|
140
|
+
// (dynamically imported). Stub it so the deferred valve never drags the heavy
|
|
141
|
+
// daemon module graph into this test process; `undefined` = "conversation not
|
|
142
|
+
// live" (the valve skips the live strip).
|
|
143
|
+
mock.module("../../../../daemon/conversation-registry.js", () => ({
|
|
144
|
+
findConversationOrSubagent: () => undefined,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
mock.module("../../../../config/assistant-feature-flags.js", () => ({
|
|
148
|
+
...realFlags,
|
|
149
|
+
isAssistantFeatureFlagEnabled: (
|
|
150
|
+
key: string,
|
|
151
|
+
config: Parameters<typeof realFlags.isAssistantFeatureFlagEnabled>[1],
|
|
152
|
+
) => {
|
|
153
|
+
if (!injectionMockActive) {
|
|
154
|
+
return realFlags.isAssistantFeatureFlagEnabled(
|
|
155
|
+
key as Parameters<typeof realFlags.isAssistantFeatureFlagEnabled>[0],
|
|
156
|
+
config,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return key === "memory-v3-live"
|
|
160
|
+
? liveEnabled
|
|
161
|
+
: key === "memory-v3-shadow"
|
|
162
|
+
? shadowEnabled
|
|
163
|
+
: false;
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
mock.module("../page-content.js", () => ({
|
|
168
|
+
...realPageContent,
|
|
169
|
+
renderV3CardContent: async (slug: Slug) =>
|
|
170
|
+
injectionMockActive
|
|
171
|
+
? slug === "missing-page"
|
|
172
|
+
? ""
|
|
173
|
+
: `# memory/concepts/${slug}.md\ncard body for ${slug}`
|
|
174
|
+
: realPageContent.renderV3CardContent(slug),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
mock.module("../shadow-plugin.js", () => ({
|
|
178
|
+
...realShadowPlugin,
|
|
179
|
+
observeTurn: (conversationId: string, turnIndex: number) =>
|
|
180
|
+
injectionMockActive
|
|
181
|
+
? observeTurnSpy(conversationId, turnIndex)
|
|
182
|
+
: realShadowPlugin.observeTurn(conversationId, turnIndex),
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
const {
|
|
186
|
+
memoryV3Injector,
|
|
187
|
+
memoryV3SpotlightInjector,
|
|
188
|
+
resetMemoryV3InjectorStateForTests,
|
|
189
|
+
} = await import("../injector.js");
|
|
190
|
+
const {
|
|
191
|
+
getActiveSlugs,
|
|
192
|
+
getInjected,
|
|
193
|
+
getPrunedSlugs,
|
|
194
|
+
markPruned,
|
|
195
|
+
recordInjected,
|
|
196
|
+
} = await import("../ever-injected-store.js");
|
|
197
|
+
const { V3_CARDS_INJECTION_HEADER } = await import("../render-injection.js");
|
|
198
|
+
const { flushPruneValveForTests } = await import("../prune.js");
|
|
199
|
+
|
|
200
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function section(slug: Slug, title: string, text: string): Section {
|
|
203
|
+
return { article: slug, title, text, ordinal: 1 };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** An orchestrate result selecting `slugs`, with optional finder-matched
|
|
207
|
+
* sections (slug → section) feeding the spotlight. */
|
|
208
|
+
function result(
|
|
209
|
+
slugs: Slug[],
|
|
210
|
+
matched: Array<[Slug, Section]> = [],
|
|
211
|
+
): OrchestrateResult {
|
|
212
|
+
return {
|
|
213
|
+
selections: slugs.map((slug) => ({ slug, pinned: false })),
|
|
214
|
+
matchedSections: new Map(matched),
|
|
215
|
+
lanes: {
|
|
216
|
+
core: [],
|
|
217
|
+
hot: [],
|
|
218
|
+
finder: matched.map(([slug]) => ({
|
|
219
|
+
slug,
|
|
220
|
+
descriptor: "",
|
|
221
|
+
lane: "needle" as const,
|
|
222
|
+
})),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const GUARDIAN_TRUST = {
|
|
228
|
+
sourceChannel: "vellum",
|
|
229
|
+
trustClass: "guardian",
|
|
230
|
+
} as const;
|
|
231
|
+
|
|
232
|
+
/** Invoke the block's attachment-commit callback — simulating runtime
|
|
233
|
+
* assembly's user-tail commit point, where the everInjected-store write
|
|
234
|
+
* (and the prune-valve schedule) now happens. */
|
|
235
|
+
function commitCardsBlock(block: InjectionBlock | null): void {
|
|
236
|
+
const commit = block?.meta?.[MEMORY_V3_COMMIT_META_KEY];
|
|
237
|
+
if (typeof commit === "function") (commit as () => void)();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Produce the cards block WITHOUT committing — what assembly observes on a
|
|
241
|
+
* turn whose tail is not a user message (the block never attaches). */
|
|
242
|
+
function produceCardsWithoutCommit(
|
|
243
|
+
conversationId: string,
|
|
244
|
+
turnIndex: number,
|
|
245
|
+
trust: { sourceChannel: string; trustClass: string } = GUARDIAN_TRUST,
|
|
246
|
+
) {
|
|
247
|
+
return memoryV3Injector.produce({
|
|
248
|
+
requestId: "req-1",
|
|
249
|
+
conversationId,
|
|
250
|
+
turnIndex,
|
|
251
|
+
trust: trust as never,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Produce the cards block and commit it (the normal user-tail turn). */
|
|
256
|
+
async function produceCards(conversationId: string, turnIndex: number) {
|
|
257
|
+
const block = await produceCardsWithoutCommit(conversationId, turnIndex);
|
|
258
|
+
commitCardsBlock(block);
|
|
259
|
+
return block;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Persist a produced card block to message metadata, as the conversation
|
|
263
|
+
* assembly does in production (unwrapped, under `memoryV3InjectedBlock`) —
|
|
264
|
+
* the prune valve's plan only counts slugs locatable in persisted rows. */
|
|
265
|
+
let persistedMessageSeq = 0;
|
|
266
|
+
function persistCardBlockMetadata(
|
|
267
|
+
conversationId: string,
|
|
268
|
+
blockText: string,
|
|
269
|
+
): void {
|
|
270
|
+
testSqlite
|
|
271
|
+
.query(
|
|
272
|
+
/*sql*/ `
|
|
273
|
+
INSERT INTO messages (id, conversation_id, role, content, metadata, created_at)
|
|
274
|
+
VALUES (?, ?, 'user', '[]', ?, 0)
|
|
275
|
+
`,
|
|
276
|
+
)
|
|
277
|
+
.run(
|
|
278
|
+
`m-${persistedMessageSeq++}`,
|
|
279
|
+
conversationId,
|
|
280
|
+
JSON.stringify({ memoryV3InjectedBlock: unwrapMemoryBlock(blockText) }),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function produceSpotlight(
|
|
285
|
+
conversationId: string,
|
|
286
|
+
turnIndex: number,
|
|
287
|
+
trust: { sourceChannel: string; trustClass: string } = GUARDIAN_TRUST,
|
|
288
|
+
) {
|
|
289
|
+
return memoryV3SpotlightInjector.produce({
|
|
290
|
+
requestId: "req-1",
|
|
291
|
+
conversationId,
|
|
292
|
+
turnIndex,
|
|
293
|
+
trust: trust as never,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
beforeEach(async () => {
|
|
298
|
+
// Drain any prune-valve work the previous test's live injection deferred,
|
|
299
|
+
// so it lands against that test's DB instead of bleeding into this one.
|
|
300
|
+
await flushPruneValveForTests();
|
|
301
|
+
injectionMockActive = true;
|
|
302
|
+
liveEnabled = false;
|
|
303
|
+
shadowEnabled = false;
|
|
304
|
+
memoryEnabled = true;
|
|
305
|
+
spotlightConfig = { n: 6, windowTurns: 2 };
|
|
306
|
+
pruneConfig = null;
|
|
307
|
+
turnResults = new Map();
|
|
308
|
+
observeTurnSpy.mockClear();
|
|
309
|
+
logCalls.length = 0;
|
|
310
|
+
testDb = makeDb();
|
|
311
|
+
resetMemoryV3InjectorStateForTests();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
afterAll(async () => {
|
|
315
|
+
// Deferred valve work must finish while the mocks are still active.
|
|
316
|
+
await flushPruneValveForTests();
|
|
317
|
+
injectionMockActive = false;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ─── frozen net-new cards ───────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
describe("memoryV3Injector — frozen net-new cards", () => {
|
|
323
|
+
test("global memory disabled → both injectors produce null without orchestration", async () => {
|
|
324
|
+
liveEnabled = true;
|
|
325
|
+
memoryEnabled = false;
|
|
326
|
+
turnResults.set(0, result(["page-a"]));
|
|
327
|
+
|
|
328
|
+
expect(await produceCardsWithoutCommit("conv-1", 0)).toBeNull();
|
|
329
|
+
expect(await produceSpotlight("conv-1", 0)).toBeNull();
|
|
330
|
+
expect(observeTurnSpy).not.toHaveBeenCalled();
|
|
331
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("turn 1 renders cards; turn 2 re-selecting the same pages renders ZERO new cards", async () => {
|
|
335
|
+
liveEnabled = true;
|
|
336
|
+
turnResults.set(0, result(["page-a", "page-b"]));
|
|
337
|
+
turnResults.set(1, result(["page-a", "page-b"]));
|
|
338
|
+
|
|
339
|
+
const t1 = await produceCards("conv-1", 0);
|
|
340
|
+
expect(t1).not.toBeNull();
|
|
341
|
+
expect(t1!.placement).toBe("after-memory-prefix");
|
|
342
|
+
expect(t1!.text.startsWith("<memory>\n")).toBe(true);
|
|
343
|
+
expect(t1!.text).toContain(V3_CARDS_INJECTION_HEADER);
|
|
344
|
+
expect(t1!.text).toContain("# memory/concepts/page-a.md");
|
|
345
|
+
expect(t1!.text).toContain("# memory/concepts/page-b.md");
|
|
346
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a", "page-b"]));
|
|
347
|
+
// Recorded bytes match the rendered card sizes (non-zero).
|
|
348
|
+
for (const entry of getInjected("conv-1").values()) {
|
|
349
|
+
expect(entry.bytes).toBeGreaterThan(0);
|
|
350
|
+
expect(entry.prunedAt).toBeNull();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// All-repeat turn: the block is still PRODUCED (its presence keys v2
|
|
354
|
+
// suppression) but carries no text — no new persistent bytes.
|
|
355
|
+
const t2 = await produceCards("conv-1", 1);
|
|
356
|
+
expect(t2).not.toBeNull();
|
|
357
|
+
expect(t2!.text).toBe("");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a partially-new turn renders only the net-new cards", async () => {
|
|
361
|
+
liveEnabled = true;
|
|
362
|
+
turnResults.set(0, result(["page-a"]));
|
|
363
|
+
turnResults.set(1, result(["page-a", "page-c"]));
|
|
364
|
+
|
|
365
|
+
await produceCards("conv-1", 0);
|
|
366
|
+
const t2 = await produceCards("conv-1", 1);
|
|
367
|
+
expect(t2!.text).toContain("# memory/concepts/page-c.md");
|
|
368
|
+
expect(t2!.text).not.toContain("# memory/concepts/page-a.md");
|
|
369
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a", "page-c"]));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("fork-seeded dedup record suppresses re-rendering inherited slugs", async () => {
|
|
373
|
+
liveEnabled = true;
|
|
374
|
+
// PR 4's fork hooks seed the child's record from inherited metadata
|
|
375
|
+
// blocks; from the injector's perspective that is just pre-existing rows.
|
|
376
|
+
recordInjected("conv-fork", [{ slug: "page-a", bytes: 0 }]);
|
|
377
|
+
turnResults.set(0, result(["page-a", "page-b"]));
|
|
378
|
+
|
|
379
|
+
const block = await produceCards("conv-fork", 0);
|
|
380
|
+
expect(block!.text).toContain("# memory/concepts/page-b.md");
|
|
381
|
+
expect(block!.text).not.toContain("# memory/concepts/page-a.md");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("a pruned slug that is re-selected re-injects as a fresh card", async () => {
|
|
385
|
+
liveEnabled = true;
|
|
386
|
+
turnResults.set(0, result(["page-a"]));
|
|
387
|
+
turnResults.set(1, result(["page-a"]));
|
|
388
|
+
|
|
389
|
+
await produceCards("conv-1", 0);
|
|
390
|
+
markPruned("conv-1", ["page-a"], Date.now());
|
|
391
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
392
|
+
|
|
393
|
+
const t2 = await produceCards("conv-1", 1);
|
|
394
|
+
expect(t2!.text).toContain("# memory/concepts/page-a.md");
|
|
395
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("end-of-turn prune valve: fires deferred after live injection, exempting core/hot lanes", async () => {
|
|
399
|
+
liveEnabled = true;
|
|
400
|
+
// Each stubbed card is ~47 bytes; cap so the second turn tips over and
|
|
401
|
+
// one prune reaches the target.
|
|
402
|
+
pruneConfig = { maxResidentBytes: 60, targetResidentBytes: 50 };
|
|
403
|
+
const t0 = result(["page-a"]);
|
|
404
|
+
t0.lanes.core = ["page-a"]; // page-a is a core-lane member this turn…
|
|
405
|
+
turnResults.set(0, t0);
|
|
406
|
+
turnResults.set(1, result(["page-b"])); // …but not on turn 1's lanes.
|
|
407
|
+
|
|
408
|
+
const b0 = await produceCards("conv-1", 0);
|
|
409
|
+
persistCardBlockMetadata("conv-1", b0!.text);
|
|
410
|
+
await flushPruneValveForTests();
|
|
411
|
+
// Turn 0 is within the cap — nothing pruned.
|
|
412
|
+
expect(getPrunedSlugs("conv-1").size).toBe(0);
|
|
413
|
+
|
|
414
|
+
const b1 = await produceCards("conv-1", 1);
|
|
415
|
+
persistCardBlockMetadata("conv-1", b1!.text);
|
|
416
|
+
// The valve is DEFERRED: nothing pruned synchronously at produce time.
|
|
417
|
+
expect(getPrunedSlugs("conv-1").size).toBe(0);
|
|
418
|
+
await flushPruneValveForTests();
|
|
419
|
+
// Over the cap, page-a (oldest, and no longer lane-exempt) is pruned.
|
|
420
|
+
expect(getPrunedSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
421
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-b"]));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("slugs whose card renders empty are neither attached nor recorded", async () => {
|
|
425
|
+
liveEnabled = true;
|
|
426
|
+
turnResults.set(0, result(["missing-page", "page-a"]));
|
|
427
|
+
|
|
428
|
+
const block = await produceCards("conv-1", 0);
|
|
429
|
+
expect(block!.text).toContain("# memory/concepts/page-a.md");
|
|
430
|
+
expect(block!.text).not.toContain("missing-page");
|
|
431
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("EVERY net-new card rendering empty → null (v2 fallback), not an empty block", async () => {
|
|
435
|
+
liveEnabled = true;
|
|
436
|
+
turnResults.set(0, result(["missing-page"]));
|
|
437
|
+
|
|
438
|
+
// An empty-text block would suppress v2 with nothing to show — a
|
|
439
|
+
// memory-less turn. Distinct from the all-repeat case (empty netNew),
|
|
440
|
+
// where the empty block correctly keeps v2 suppressed.
|
|
441
|
+
expect(await produceCards("conv-1", 0)).toBeNull();
|
|
442
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("produce() defers the store write to the commit callback — a never-attached block records nothing", async () => {
|
|
446
|
+
liveEnabled = true;
|
|
447
|
+
turnResults.set(0, result(["page-a"]));
|
|
448
|
+
|
|
449
|
+
// A turn whose tail is not a user message: assembly never invokes the
|
|
450
|
+
// commit, so the store must not claim the cards (which would suppress
|
|
451
|
+
// them until compaction despite never reaching history).
|
|
452
|
+
const block = await produceCardsWithoutCommit("conv-1", 0);
|
|
453
|
+
expect(block).not.toBeNull();
|
|
454
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
455
|
+
|
|
456
|
+
// Assembly's user-tail commit point records them.
|
|
457
|
+
commitCardsBlock(block);
|
|
458
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("untrusted remote actor → both injectors produce null, no orchestration, nothing recorded", async () => {
|
|
462
|
+
liveEnabled = true;
|
|
463
|
+
turnResults.set(0, result(["page-a"]));
|
|
464
|
+
const untrusted = { sourceChannel: "telegram", trustClass: "unknown" };
|
|
465
|
+
|
|
466
|
+
expect(await produceCardsWithoutCommit("conv-1", 0, untrusted)).toBeNull();
|
|
467
|
+
expect(await produceSpotlight("conv-1", 0, untrusted)).toBeNull();
|
|
468
|
+
// The gate runs before orchestration: nothing selected, nothing recorded.
|
|
469
|
+
expect(observeTurnSpy).not.toHaveBeenCalled();
|
|
470
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("capability cards (skills / CLI commands) record ZERO bytes", async () => {
|
|
474
|
+
liveEnabled = true;
|
|
475
|
+
turnResults.set(0, result(["skills/test-skill", "page-a"]));
|
|
476
|
+
|
|
477
|
+
const block = await produceCards("conv-1", 0);
|
|
478
|
+
// Both cards attach…
|
|
479
|
+
expect(block!.text).toContain("skills/test-skill");
|
|
480
|
+
expect(block!.text).toContain("# memory/concepts/page-a.md");
|
|
481
|
+
// …but the capability card's bytes are recorded as 0: its `# Skill:`
|
|
482
|
+
// header is invisible to the prune valve's `# memory/concepts/<slug>.md`
|
|
483
|
+
// section grammar, so non-zero bytes could never be freed and would
|
|
484
|
+
// loop-fire the valve.
|
|
485
|
+
const injected = getInjected("conv-1");
|
|
486
|
+
expect(injected.get("skills/test-skill")!.bytes).toBe(0);
|
|
487
|
+
expect(injected.get("page-a")!.bytes).toBeGreaterThan(0);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("per-conversation memo LRU: a key refresh evicts nothing; new-key eviction prefers stale entries", async () => {
|
|
491
|
+
liveEnabled = true;
|
|
492
|
+
turnResults.set(0, result(["page-a"]));
|
|
493
|
+
turnResults.set(1, result(["page-a"]));
|
|
494
|
+
// Fill the memo to its 256-entry cap.
|
|
495
|
+
for (let i = 0; i < 256; i++) {
|
|
496
|
+
await produceCards(`conv-${i}`, 0);
|
|
497
|
+
}
|
|
498
|
+
// A new turn for a tracked conversation is a key REFRESH — nothing may be
|
|
499
|
+
// evicted for it (the pre-fix code evicted the oldest entry here).
|
|
500
|
+
await produceCards("conv-5", 1);
|
|
501
|
+
observeTurnSpy.mockClear();
|
|
502
|
+
await produceCards("conv-0", 0);
|
|
503
|
+
expect(observeTurnSpy).toHaveBeenCalledTimes(0); // still memoized
|
|
504
|
+
// A genuinely NEW key at the cap evicts the least-recently-set entry
|
|
505
|
+
// (conv-0); the refreshed conv-5 survives.
|
|
506
|
+
await produceCards("conv-new", 0);
|
|
507
|
+
observeTurnSpy.mockClear();
|
|
508
|
+
await produceCards("conv-5", 1);
|
|
509
|
+
expect(observeTurnSpy).toHaveBeenCalledTimes(0); // refreshed → survived
|
|
510
|
+
await produceCards("conv-0", 0);
|
|
511
|
+
expect(observeTurnSpy).toHaveBeenCalledTimes(1); // evicted → re-observed
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("empty selection → null (fallback to v2), nothing recorded", async () => {
|
|
515
|
+
liveEnabled = true;
|
|
516
|
+
turnResults.set(0, result([]));
|
|
517
|
+
expect(await produceCards("conv-1", 0)).toBeNull();
|
|
518
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("shadow mode (live off) attaches nothing, records nothing, logs the would-inject set", async () => {
|
|
522
|
+
shadowEnabled = true;
|
|
523
|
+
turnResults.set(
|
|
524
|
+
0,
|
|
525
|
+
result(
|
|
526
|
+
["page-a"],
|
|
527
|
+
[["page-a", section("page-a", "Heading", "section text")]],
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(await produceCards("conv-1", 0)).toBeNull();
|
|
532
|
+
expect(await produceSpotlight("conv-1", 0)).toBeNull();
|
|
533
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set());
|
|
534
|
+
const shadowLog = logCalls.find((c) =>
|
|
535
|
+
c.msg.includes("memory-v3 shadow: would inject"),
|
|
536
|
+
);
|
|
537
|
+
expect(shadowLog).toBeDefined();
|
|
538
|
+
const data = shadowLog!.data as {
|
|
539
|
+
netNew: Array<{ slug: string; bytes: number }>;
|
|
540
|
+
spotlightRefs: string[];
|
|
541
|
+
};
|
|
542
|
+
expect(data.netNew.map((e) => e.slug)).toEqual(["page-a"]);
|
|
543
|
+
expect(data.spotlightRefs).toEqual(["page-a§Heading"]);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("the persistent card block never contains the spotlight wrapper", async () => {
|
|
547
|
+
liveEnabled = true;
|
|
548
|
+
turnResults.set(
|
|
549
|
+
0,
|
|
550
|
+
result(
|
|
551
|
+
["page-a"],
|
|
552
|
+
[["page-a", section("page-a", "Heading", "section text")]],
|
|
553
|
+
),
|
|
554
|
+
);
|
|
555
|
+
const block = await produceCards("conv-1", 0);
|
|
556
|
+
expect(block!.text).not.toContain("<memory_spotlight>");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("both injectors share ONE orchestration per turn (memoized)", async () => {
|
|
560
|
+
liveEnabled = true;
|
|
561
|
+
turnResults.set(0, result(["page-a"]));
|
|
562
|
+
await produceCards("conv-1", 0);
|
|
563
|
+
await produceSpotlight("conv-1", 0);
|
|
564
|
+
expect(observeTurnSpy).toHaveBeenCalledTimes(1);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// ─── ephemeral spotlight ────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
describe("memoryV3SpotlightInjector — ephemeral section spotlight", () => {
|
|
571
|
+
const sectionA = section("page-a", "Alpha", "alpha section text");
|
|
572
|
+
const sectionB = section("page-b", "Beta", "beta section text");
|
|
573
|
+
const sectionC = section("page-c", "Gamma", "gamma section text");
|
|
574
|
+
|
|
575
|
+
test("renders selected finder hits' matched sections at the user tail", async () => {
|
|
576
|
+
liveEnabled = true;
|
|
577
|
+
turnResults.set(0, result(["page-a", "page-b"], [["page-a", sectionA]]));
|
|
578
|
+
|
|
579
|
+
const block = await produceSpotlight("conv-1", 0);
|
|
580
|
+
expect(block).not.toBeNull();
|
|
581
|
+
expect(block!.placement).toBe("append-user-tail");
|
|
582
|
+
expect(block!.text.startsWith("<memory_spotlight>\n")).toBe(true);
|
|
583
|
+
expect(block!.text.endsWith("\n</memory_spotlight>")).toBe(true);
|
|
584
|
+
expect(block!.text).toContain(
|
|
585
|
+
"## memory/concepts/page-a.md § Alpha\nalpha section text",
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("unselected finder hits do not spotlight; top-n bound applies", async () => {
|
|
590
|
+
liveEnabled = true;
|
|
591
|
+
spotlightConfig = { n: 1, windowTurns: 0 };
|
|
592
|
+
turnResults.set(
|
|
593
|
+
0,
|
|
594
|
+
result(
|
|
595
|
+
["page-a", "page-b"],
|
|
596
|
+
[
|
|
597
|
+
["page-c", sectionC], // finder hit, NOT selected
|
|
598
|
+
["page-a", sectionA],
|
|
599
|
+
["page-b", sectionB], // selected but over the n=1 cut
|
|
600
|
+
],
|
|
601
|
+
),
|
|
602
|
+
);
|
|
603
|
+
const block = await produceSpotlight("conv-1", 0);
|
|
604
|
+
expect(block!.text).toContain("page-a.md § Alpha");
|
|
605
|
+
expect(block!.text).not.toContain("page-b.md");
|
|
606
|
+
expect(block!.text).not.toContain("page-c.md");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("carries the previous windowTurns turns' entries, ages older ones out, never accumulates", async () => {
|
|
610
|
+
liveEnabled = true;
|
|
611
|
+
spotlightConfig = { n: 6, windowTurns: 1 };
|
|
612
|
+
turnResults.set(0, result(["page-a"], [["page-a", sectionA]]));
|
|
613
|
+
turnResults.set(1, result(["page-b"], [["page-b", sectionB]]));
|
|
614
|
+
turnResults.set(2, result(["page-c"], [["page-c", sectionC]]));
|
|
615
|
+
|
|
616
|
+
const t1 = await produceSpotlight("conv-1", 0);
|
|
617
|
+
expect(t1!.text).toContain("Alpha");
|
|
618
|
+
|
|
619
|
+
// Turn 1: current (Beta) + previous turn (Alpha).
|
|
620
|
+
const t2 = await produceSpotlight("conv-1", 1);
|
|
621
|
+
expect(t2!.text).toContain("Beta");
|
|
622
|
+
expect(t2!.text).toContain("Alpha");
|
|
623
|
+
|
|
624
|
+
// Turn 2: current (Gamma) + turn 1 (Beta); turn 0 (Alpha) aged out.
|
|
625
|
+
const t3 = await produceSpotlight("conv-1", 2);
|
|
626
|
+
expect(t3!.text).toContain("Gamma");
|
|
627
|
+
expect(t3!.text).toContain("Beta");
|
|
628
|
+
expect(t3!.text).not.toContain("Alpha");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("re-producing the SAME turn replaces its window entry (no duplication)", async () => {
|
|
632
|
+
liveEnabled = true;
|
|
633
|
+
spotlightConfig = { n: 6, windowTurns: 2 };
|
|
634
|
+
turnResults.set(0, result(["page-a"], [["page-a", sectionA]]));
|
|
635
|
+
|
|
636
|
+
await produceSpotlight("conv-1", 0);
|
|
637
|
+
const again = await produceSpotlight("conv-1", 0);
|
|
638
|
+
const occurrences = again!.text.split("page-a.md § Alpha").length - 1;
|
|
639
|
+
expect(occurrences).toBe(1);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("window is bounded by n × (windowTurns + 1) entries", async () => {
|
|
643
|
+
liveEnabled = true;
|
|
644
|
+
spotlightConfig = { n: 2, windowTurns: 1 };
|
|
645
|
+
const sections = (turn: number): Array<[Slug, Section]> =>
|
|
646
|
+
["w", "x", "y", "z"].map((s) => {
|
|
647
|
+
const slug = `page-${s}${turn}`;
|
|
648
|
+
return [slug, section(slug, `T${turn}${s}`, "text")] as [Slug, Section];
|
|
649
|
+
});
|
|
650
|
+
turnResults.set(
|
|
651
|
+
0,
|
|
652
|
+
result(
|
|
653
|
+
sections(0).map(([s]) => s),
|
|
654
|
+
sections(0),
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
turnResults.set(
|
|
658
|
+
1,
|
|
659
|
+
result(
|
|
660
|
+
sections(1).map(([s]) => s),
|
|
661
|
+
sections(1),
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
await produceSpotlight("conv-1", 0);
|
|
666
|
+
const block = await produceSpotlight("conv-1", 1);
|
|
667
|
+
const entryCount = (block!.text.match(/^## memory\/concepts\//gm) ?? [])
|
|
668
|
+
.length;
|
|
669
|
+
expect(entryCount).toBeLessThanOrEqual(2 * (1 + 1));
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("no matched sections in the window → no block", async () => {
|
|
673
|
+
liveEnabled = true;
|
|
674
|
+
turnResults.set(0, result(["page-a"]));
|
|
675
|
+
expect(await produceSpotlight("conv-1", 0)).toBeNull();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("live off → null even when shadow is on", async () => {
|
|
679
|
+
shadowEnabled = true;
|
|
680
|
+
turnResults.set(0, result(["page-a"], [["page-a", sectionA]]));
|
|
681
|
+
expect(await produceSpotlight("conv-1", 0)).toBeNull();
|
|
682
|
+
});
|
|
683
|
+
});
|