@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,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `prune.ts` — the memory-v3 resident-footprint prune valve:
|
|
3
|
+
* - `filterPrunedCardSections`: card-boundary parsing, byte-identical
|
|
4
|
+
* remainders, all-pruned → `""`, no-op → same reference, capability
|
|
5
|
+
* chunks (`# Skill:` / `# CLI command:` headers) terminating a card and
|
|
6
|
+
* surviving its prune;
|
|
7
|
+
* - `planPrune`: no-op below the cap, oldest-first selection-recency
|
|
8
|
+
* ranking down to the target, core/hot exemption, `injected_at` fallback,
|
|
9
|
+
* zero-byte (capability / fork-seed) rows skipped, idempotence below the
|
|
10
|
+
* cap;
|
|
11
|
+
* - `runPruneValve` + the live strip: v3-owned blocks stripped in place,
|
|
12
|
+
* v2-lookalike blocks untouched, all-pruned blocks removed, and the
|
|
13
|
+
* rehydration filter (the same `filterPrunedCardSections` over persisted
|
|
14
|
+
* metadata) converging to the same bytes;
|
|
15
|
+
* - re-injection round-trip: `recordInjected` clears `pruned_at`, after
|
|
16
|
+
* which the filter keeps the slug again;
|
|
17
|
+
* - accounting-drift regression: a slug with recorded bytes but no
|
|
18
|
+
* locatable persisted card section is tombstoned in ONE pass and the
|
|
19
|
+
* valve does not loop-fire afterwards;
|
|
20
|
+
* - `schedulePruneValve` deferred execution via `flushPruneValveForTests`.
|
|
21
|
+
*
|
|
22
|
+
* `mock.module` is process-global and leaks into sibling files in a directory
|
|
23
|
+
* run, so the db-connection / config stubs DELEGATE to the real implementation
|
|
24
|
+
* unless this test is actively running (`pruneMockActive`) — mirrors
|
|
25
|
+
* `ever-injected-store.test.ts`.
|
|
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 { wrapMemoryBlock } 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 { Message } from "../../../providers/types.js";
|
|
38
|
+
|
|
39
|
+
const realDb = { ...(await import("../../../memory/db-connection.js")) };
|
|
40
|
+
const realConfigLoader = { ...(await import("../../../config/loader.js")) };
|
|
41
|
+
|
|
42
|
+
let pruneMockActive = false;
|
|
43
|
+
let pruneConfig: {
|
|
44
|
+
maxResidentBytes: number;
|
|
45
|
+
targetResidentBytes: number;
|
|
46
|
+
} | null = null;
|
|
47
|
+
|
|
48
|
+
let testSqlite: Database;
|
|
49
|
+
let testDb = makeDb();
|
|
50
|
+
function makeDb() {
|
|
51
|
+
testSqlite = new Database(":memory:");
|
|
52
|
+
const db = drizzle(testSqlite, { schema });
|
|
53
|
+
migrateAddMemoryV3EverInjected(db);
|
|
54
|
+
migrateAddMemoryV3Selections(db);
|
|
55
|
+
// Minimal `messages` shape — `collectPersistedV3Cards` reads only
|
|
56
|
+
// `conversation_id` and `metadata`.
|
|
57
|
+
testSqlite.run(/*sql*/ `
|
|
58
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
conversation_id TEXT NOT NULL,
|
|
61
|
+
role TEXT NOT NULL,
|
|
62
|
+
content TEXT NOT NULL,
|
|
63
|
+
metadata TEXT,
|
|
64
|
+
created_at INTEGER NOT NULL
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
67
|
+
return db;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
mock.module("../../../memory/db-connection.js", () => ({
|
|
71
|
+
...realDb,
|
|
72
|
+
getDb: () => (pruneMockActive ? testDb : realDb.getDb()),
|
|
73
|
+
getSqliteFrom: (db: unknown) =>
|
|
74
|
+
pruneMockActive
|
|
75
|
+
? testSqlite
|
|
76
|
+
: realDb.getSqliteFrom(db as Parameters<typeof realDb.getSqliteFrom>[0]),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
mock.module("../../../config/loader.js", () => ({
|
|
80
|
+
...realConfigLoader,
|
|
81
|
+
getConfig: () =>
|
|
82
|
+
pruneMockActive
|
|
83
|
+
? { memory: { v3: { prune: pruneConfig ?? undefined } } }
|
|
84
|
+
: realConfigLoader.getConfig(),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
collectPersistedV3Cards,
|
|
89
|
+
filterPrunedCardSections,
|
|
90
|
+
flushPruneValveForTests,
|
|
91
|
+
parseCardSections,
|
|
92
|
+
planPrune,
|
|
93
|
+
runPruneValve,
|
|
94
|
+
schedulePruneValve,
|
|
95
|
+
stripPrunedCardsFromMessages,
|
|
96
|
+
} = await import("./prune.js");
|
|
97
|
+
const { getActiveSlugs, getInjected, getPrunedSlugs, recordInjected } =
|
|
98
|
+
await import("./ever-injected-store.js");
|
|
99
|
+
const { V3_CARDS_INJECTION_HEADER, renderCardsBlockInner } =
|
|
100
|
+
await import("./render-injection.js");
|
|
101
|
+
|
|
102
|
+
// ─── fixtures ────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/** A card exactly as `renderCard` shapes it: path header, head, TOC line. */
|
|
105
|
+
function card(slug: string): string {
|
|
106
|
+
return `# memory/concepts/${slug}.md\nlead for ${slug}\n\n[sections: §One · §Two]`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A capability chunk exactly as `renderCapabilityContent` shapes it: its own
|
|
110
|
+
* non-concept top-level header plus the capability content. */
|
|
111
|
+
const CAPABILITY_CHUNK = "# Skill: meet-join\nJoin a video meeting on request.";
|
|
112
|
+
|
|
113
|
+
function insertSelection(
|
|
114
|
+
conversationId: string,
|
|
115
|
+
turn: number,
|
|
116
|
+
slug: string,
|
|
117
|
+
createdAt: number,
|
|
118
|
+
): void {
|
|
119
|
+
testSqlite
|
|
120
|
+
.query(
|
|
121
|
+
/*sql*/ `
|
|
122
|
+
INSERT OR REPLACE INTO memory_v3_selections
|
|
123
|
+
(conversation_id, turn, slug, source, pinned, created_at)
|
|
124
|
+
VALUES (?, ?, ?, 'needle', 0, ?)
|
|
125
|
+
`,
|
|
126
|
+
)
|
|
127
|
+
.run(conversationId, turn, slug, createdAt);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function insertUserRowWithV3Block(
|
|
131
|
+
conversationId: string,
|
|
132
|
+
id: string,
|
|
133
|
+
blockInner: string,
|
|
134
|
+
): void {
|
|
135
|
+
testSqlite
|
|
136
|
+
.query(
|
|
137
|
+
/*sql*/ `
|
|
138
|
+
INSERT INTO messages (id, conversation_id, role, content, metadata, created_at)
|
|
139
|
+
VALUES (?, ?, 'user', '[]', ?, 0)
|
|
140
|
+
`,
|
|
141
|
+
)
|
|
142
|
+
.run(
|
|
143
|
+
id,
|
|
144
|
+
conversationId,
|
|
145
|
+
JSON.stringify({ memoryV3InjectedBlock: blockInner }),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
pruneMockActive = true;
|
|
151
|
+
pruneConfig = null;
|
|
152
|
+
testDb = makeDb();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterAll(async () => {
|
|
156
|
+
await flushPruneValveForTests();
|
|
157
|
+
pruneMockActive = false;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── filterPrunedCardSections ────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("parseCardSections / filterPrunedCardSections", () => {
|
|
163
|
+
const inner = renderCardsBlockInner([
|
|
164
|
+
card("page-a"),
|
|
165
|
+
card("page-b"),
|
|
166
|
+
card("page-c"),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
test("parses preamble and per-card sections at the path headers", () => {
|
|
170
|
+
const parsed = parseCardSections(inner);
|
|
171
|
+
expect(parsed.preamble).toBe(V3_CARDS_INJECTION_HEADER);
|
|
172
|
+
expect(parsed.sections.map((s) => s.slug)).toEqual([
|
|
173
|
+
"page-a",
|
|
174
|
+
"page-b",
|
|
175
|
+
"page-c",
|
|
176
|
+
]);
|
|
177
|
+
expect(parsed.sections[1]!.text).toBe(card("page-b"));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("no pruned slug present → returns the SAME reference (no-op)", () => {
|
|
181
|
+
expect(filterPrunedCardSections(inner, new Set(["page-z"]))).toBe(inner);
|
|
182
|
+
expect(filterPrunedCardSections(inner, new Set())).toBe(inner);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("strips pruned cards, leaving the remainder byte-identical to a fresh render", () => {
|
|
186
|
+
const filtered = filterPrunedCardSections(inner, new Set(["page-b"]));
|
|
187
|
+
expect(filtered).toBe(
|
|
188
|
+
renderCardsBlockInner([card("page-a"), card("page-c")]),
|
|
189
|
+
);
|
|
190
|
+
// A `# Title` line inside a card head is NOT a card boundary.
|
|
191
|
+
const headerInHead = `${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\n# A Title Line\nlead\n\n# memory/concepts/page-b.md\nlead b`;
|
|
192
|
+
expect(filterPrunedCardSections(headerInHead, new Set(["page-b"]))).toBe(
|
|
193
|
+
`${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\n# A Title Line\nlead`,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("all cards pruned → empty string (caller drops the block)", () => {
|
|
198
|
+
expect(
|
|
199
|
+
filterPrunedCardSections(inner, new Set(["page-a", "page-b", "page-c"])),
|
|
200
|
+
).toBe("");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("text with no card headers passes through unchanged", () => {
|
|
204
|
+
const plain = "remember: user prefers tea";
|
|
205
|
+
expect(filterPrunedCardSections(plain, new Set(["page-a"]))).toBe(plain);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a capability chunk terminates the preceding card's section", () => {
|
|
209
|
+
const mixed = renderCardsBlockInner([
|
|
210
|
+
card("page-a"),
|
|
211
|
+
CAPABILITY_CHUNK,
|
|
212
|
+
card("page-b"),
|
|
213
|
+
]);
|
|
214
|
+
const parsed = parseCardSections(mixed);
|
|
215
|
+
expect(parsed.sections.map((s) => s.slug)).toEqual(["page-a", "page-b"]);
|
|
216
|
+
// page-a's section stops AT the capability header — it must not absorb it.
|
|
217
|
+
expect(parsed.sections[0]!.text).toBe(card("page-a"));
|
|
218
|
+
expect(parsed.pieces.map((p) => p.kind)).toEqual(["card", "other", "card"]);
|
|
219
|
+
expect(parsed.pieces[1]!.text).toBe(CAPABILITY_CHUNK);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("pruning a concept card never swallows a trailing capability chunk", () => {
|
|
223
|
+
const mixed = renderCardsBlockInner([
|
|
224
|
+
card("page-a"),
|
|
225
|
+
CAPABILITY_CHUNK,
|
|
226
|
+
card("page-b"),
|
|
227
|
+
]);
|
|
228
|
+
expect(filterPrunedCardSections(mixed, new Set(["page-a"]))).toBe(
|
|
229
|
+
renderCardsBlockInner([CAPABILITY_CHUNK, card("page-b")]),
|
|
230
|
+
);
|
|
231
|
+
// Capability chunk at the block END survives the prune of the last card.
|
|
232
|
+
const trailing = renderCardsBlockInner([card("page-a"), CAPABILITY_CHUNK]);
|
|
233
|
+
expect(filterPrunedCardSections(trailing, new Set(["page-a"]))).toBe(
|
|
234
|
+
renderCardsBlockInner([CAPABILITY_CHUNK]),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("all cards pruned keeps the preamble + capability chunks", () => {
|
|
239
|
+
const mixed = renderCardsBlockInner([card("page-a"), CAPABILITY_CHUNK]);
|
|
240
|
+
expect(
|
|
241
|
+
filterPrunedCardSections(mixed, new Set(["page-a", "page-b", "page-c"])),
|
|
242
|
+
).toBe(renderCardsBlockInner([CAPABILITY_CHUNK]));
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─── planPrune ───────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
describe("planPrune", () => {
|
|
249
|
+
const deps = {
|
|
250
|
+
maxResidentBytes: 300,
|
|
251
|
+
targetResidentBytes: 200,
|
|
252
|
+
exemptSlugs: new Set<string>(),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
test("no-op below (or at) the cap", () => {
|
|
256
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 300 }], 1_000);
|
|
257
|
+
expect(planPrune(deps, "conv-1")).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("over the cap: prunes oldest-first by last selection recency down to the target", () => {
|
|
261
|
+
recordInjected(
|
|
262
|
+
"conv-1",
|
|
263
|
+
[
|
|
264
|
+
{ slug: "page-a", bytes: 100 },
|
|
265
|
+
{ slug: "page-b", bytes: 100 },
|
|
266
|
+
{ slug: "page-c", bytes: 100 },
|
|
267
|
+
{ slug: "page-d", bytes: 100 },
|
|
268
|
+
],
|
|
269
|
+
1_000,
|
|
270
|
+
);
|
|
271
|
+
insertSelection("conv-1", 0, "page-a", 1_000);
|
|
272
|
+
insertSelection("conv-1", 0, "page-b", 2_000);
|
|
273
|
+
insertSelection("conv-1", 0, "page-c", 3_000);
|
|
274
|
+
insertSelection("conv-1", 0, "page-d", 4_000);
|
|
275
|
+
|
|
276
|
+
const plan = planPrune(deps, "conv-1");
|
|
277
|
+
expect(plan).toEqual({ slugs: ["page-a", "page-b"], bytesFreed: 200 });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("re-selection recency outranks injection order", () => {
|
|
281
|
+
recordInjected(
|
|
282
|
+
"conv-1",
|
|
283
|
+
[
|
|
284
|
+
{ slug: "page-old", bytes: 200 },
|
|
285
|
+
{ slug: "page-new", bytes: 200 },
|
|
286
|
+
],
|
|
287
|
+
1_000,
|
|
288
|
+
);
|
|
289
|
+
// page-old was injected first but re-selected most recently; page-new was
|
|
290
|
+
// selected only at injection time.
|
|
291
|
+
insertSelection("conv-1", 0, "page-old", 1_000);
|
|
292
|
+
insertSelection("conv-1", 1, "page-new", 2_000);
|
|
293
|
+
insertSelection("conv-1", 5, "page-old", 9_000);
|
|
294
|
+
|
|
295
|
+
const plan = planPrune(deps, "conv-1");
|
|
296
|
+
expect(plan!.slugs).toEqual(["page-new"]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("never-selected slugs fall back to injected_at for recency", () => {
|
|
300
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 200 }], 5_000);
|
|
301
|
+
recordInjected("conv-1", [{ slug: "page-b", bytes: 200 }], 1_000);
|
|
302
|
+
|
|
303
|
+
const plan = planPrune(deps, "conv-1");
|
|
304
|
+
expect(plan!.slugs).toEqual(["page-b"]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("core/hot exempt slugs are never pruned, even when oldest", () => {
|
|
308
|
+
recordInjected(
|
|
309
|
+
"conv-1",
|
|
310
|
+
[
|
|
311
|
+
{ slug: "core-page", bytes: 150 },
|
|
312
|
+
{ slug: "page-b", bytes: 150 },
|
|
313
|
+
{ slug: "page-c", bytes: 150 },
|
|
314
|
+
],
|
|
315
|
+
1_000,
|
|
316
|
+
);
|
|
317
|
+
insertSelection("conv-1", 0, "core-page", 1_000);
|
|
318
|
+
insertSelection("conv-1", 0, "page-b", 2_000);
|
|
319
|
+
insertSelection("conv-1", 0, "page-c", 3_000);
|
|
320
|
+
|
|
321
|
+
// Resident 450 > max 300: reaching the 200 target needs both non-exempt
|
|
322
|
+
// pages — the exempt core page is skipped over even though it is oldest.
|
|
323
|
+
const plan = planPrune(
|
|
324
|
+
{ ...deps, exemptSlugs: new Set(["core-page"]) },
|
|
325
|
+
"conv-1",
|
|
326
|
+
);
|
|
327
|
+
expect(plan!.slugs).toEqual(["page-b", "page-c"]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("zero-byte rows (capability slugs, fork seeds) are skipped (pruning them frees nothing)", () => {
|
|
331
|
+
recordInjected(
|
|
332
|
+
"conv-1",
|
|
333
|
+
[
|
|
334
|
+
{ slug: "seeded", bytes: 0 },
|
|
335
|
+
{ slug: "skills/meet-join", bytes: 0 },
|
|
336
|
+
{ slug: "page-b", bytes: 400 },
|
|
337
|
+
],
|
|
338
|
+
1_000,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const plan = planPrune(deps, "conv-1");
|
|
342
|
+
expect(plan!.slugs).toEqual(["page-b"]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("returns null when only exempt/zero-byte candidates remain over the cap", () => {
|
|
346
|
+
recordInjected("conv-1", [{ slug: "core-page", bytes: 400 }], 1_000);
|
|
347
|
+
expect(
|
|
348
|
+
planPrune({ ...deps, exemptSlugs: new Set(["core-page"]) }, "conv-1"),
|
|
349
|
+
).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ─── live strip & v3-block identification ────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
describe("stripPrunedCardsFromMessages", () => {
|
|
356
|
+
const innerAB = renderCardsBlockInner([card("page-a"), card("page-b")]);
|
|
357
|
+
const knownSections = new Set([card("page-a"), card("page-b")]);
|
|
358
|
+
|
|
359
|
+
function userMessage(...texts: string[]): Message {
|
|
360
|
+
return {
|
|
361
|
+
role: "user",
|
|
362
|
+
content: texts.map((text) => ({ type: "text" as const, text })),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
test("strips pruned cards from v3-owned blocks in place", () => {
|
|
367
|
+
const message = userMessage(wrapMemoryBlock(innerAB), "hello");
|
|
368
|
+
const messages = [message];
|
|
369
|
+
|
|
370
|
+
const stripped = stripPrunedCardsFromMessages(
|
|
371
|
+
messages,
|
|
372
|
+
new Set(["page-a"]),
|
|
373
|
+
knownSections,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(stripped).toBe(1);
|
|
377
|
+
expect(message.content).toEqual([
|
|
378
|
+
{
|
|
379
|
+
type: "text",
|
|
380
|
+
text: wrapMemoryBlock(renderCardsBlockInner([card("page-b")])),
|
|
381
|
+
},
|
|
382
|
+
{ type: "text", text: "hello" },
|
|
383
|
+
]);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("removes a block whose cards are ALL pruned (matching rehydration's skip)", () => {
|
|
387
|
+
const message = userMessage(wrapMemoryBlock(innerAB), "hello");
|
|
388
|
+
|
|
389
|
+
stripPrunedCardsFromMessages(
|
|
390
|
+
[message],
|
|
391
|
+
new Set(["page-a", "page-b"]),
|
|
392
|
+
knownSections,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(message.content).toEqual([{ type: "text", text: "hello" }]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("leaves v2-lookalike blocks untouched even when they name a pruned slug", () => {
|
|
399
|
+
// Same wrapper + header convention, but the section body is a v2 SUMMARY,
|
|
400
|
+
// not a card — so it fails the known-sections ownership test.
|
|
401
|
+
const v2Inner = `${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\nv2 summary of page a`;
|
|
402
|
+
const message = userMessage(wrapMemoryBlock(v2Inner));
|
|
403
|
+
|
|
404
|
+
const stripped = stripPrunedCardsFromMessages(
|
|
405
|
+
[message],
|
|
406
|
+
new Set(["page-a"]),
|
|
407
|
+
knownSections,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(stripped).toBe(0);
|
|
411
|
+
expect(message.content).toEqual([
|
|
412
|
+
{ type: "text", text: wrapMemoryBlock(v2Inner) },
|
|
413
|
+
]);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("stripping a card from a capability-bearing v3 block keeps the capability chunk", () => {
|
|
417
|
+
const mixedInner = renderCardsBlockInner([
|
|
418
|
+
card("page-a"),
|
|
419
|
+
CAPABILITY_CHUNK,
|
|
420
|
+
card("page-b"),
|
|
421
|
+
]);
|
|
422
|
+
const message = userMessage(wrapMemoryBlock(mixedInner), "hello");
|
|
423
|
+
|
|
424
|
+
// Ownership is judged on the card sections only — the capability chunk
|
|
425
|
+
// (a non-card piece on both the persisted and live side) doesn't break it.
|
|
426
|
+
const stripped = stripPrunedCardsFromMessages(
|
|
427
|
+
[message],
|
|
428
|
+
new Set(["page-a"]),
|
|
429
|
+
knownSections,
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(stripped).toBe(1);
|
|
433
|
+
expect(message.content[0]).toEqual({
|
|
434
|
+
type: "text",
|
|
435
|
+
text: wrapMemoryBlock(
|
|
436
|
+
renderCardsBlockInner([CAPABILITY_CHUNK, card("page-b")]),
|
|
437
|
+
),
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("ignores assistant messages, non-memory blocks, and unpruned v3 blocks", () => {
|
|
442
|
+
const assistant: Message = {
|
|
443
|
+
role: "assistant",
|
|
444
|
+
content: [{ type: "text", text: wrapMemoryBlock(innerAB) }],
|
|
445
|
+
};
|
|
446
|
+
const untouched = userMessage(wrapMemoryBlock(innerAB), "tail");
|
|
447
|
+
const before = untouched.content;
|
|
448
|
+
|
|
449
|
+
const stripped = stripPrunedCardsFromMessages(
|
|
450
|
+
[assistant, untouched],
|
|
451
|
+
new Set(["page-z"]),
|
|
452
|
+
knownSections,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
expect(stripped).toBe(0);
|
|
456
|
+
// No-op leaves the original content array reference in place.
|
|
457
|
+
expect(untouched.content).toBe(before);
|
|
458
|
+
expect(assistant.content[0]).toEqual({
|
|
459
|
+
type: "text",
|
|
460
|
+
text: wrapMemoryBlock(innerAB),
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe("collectPersistedV3Cards", () => {
|
|
466
|
+
test("collects card sections from persisted v3 metadata, skipping malformed rows", () => {
|
|
467
|
+
insertUserRowWithV3Block(
|
|
468
|
+
"conv-1",
|
|
469
|
+
"m1",
|
|
470
|
+
renderCardsBlockInner([card("page-a")]),
|
|
471
|
+
);
|
|
472
|
+
insertUserRowWithV3Block(
|
|
473
|
+
"conv-1",
|
|
474
|
+
"m2",
|
|
475
|
+
renderCardsBlockInner([card("page-b")]),
|
|
476
|
+
);
|
|
477
|
+
testSqlite
|
|
478
|
+
.query(
|
|
479
|
+
/*sql*/ `
|
|
480
|
+
INSERT INTO messages (id, conversation_id, role, content, metadata, created_at)
|
|
481
|
+
VALUES ('m3', 'conv-1', 'user', '[]', 'not json memoryV3InjectedBlock', 0)
|
|
482
|
+
`,
|
|
483
|
+
)
|
|
484
|
+
.run();
|
|
485
|
+
|
|
486
|
+
expect(collectPersistedV3Cards("conv-1")).toEqual(
|
|
487
|
+
new Set([card("page-a"), card("page-b")]),
|
|
488
|
+
);
|
|
489
|
+
expect(collectPersistedV3Cards("conv-other").size).toBe(0);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("capability chunks contribute no section (they are non-card chunks)", () => {
|
|
493
|
+
insertUserRowWithV3Block(
|
|
494
|
+
"conv-1",
|
|
495
|
+
"m1",
|
|
496
|
+
renderCardsBlockInner([card("page-a"), CAPABILITY_CHUNK]),
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect(collectPersistedV3Cards("conv-1")).toEqual(
|
|
500
|
+
new Set([card("page-a")]),
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ─── runPruneValve (end-to-end against the temp DB) ──────────────────────────
|
|
506
|
+
|
|
507
|
+
describe("runPruneValve", () => {
|
|
508
|
+
test("below the cap: no-op, nothing marked pruned (idempotent)", async () => {
|
|
509
|
+
pruneConfig = { maxResidentBytes: 1_000, targetResidentBytes: 500 };
|
|
510
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 100 }], 1_000);
|
|
511
|
+
|
|
512
|
+
expect(
|
|
513
|
+
await runPruneValve("conv-1", { exemptSlugs: new Set() }),
|
|
514
|
+
).toBeNull();
|
|
515
|
+
expect(
|
|
516
|
+
await runPruneValve("conv-1", { exemptSlugs: new Set() }),
|
|
517
|
+
).toBeNull();
|
|
518
|
+
expect(getPrunedSlugs("conv-1").size).toBe(0);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("missing prune config: bails before touching the store", async () => {
|
|
522
|
+
pruneConfig = null;
|
|
523
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 100 }], 1_000);
|
|
524
|
+
expect(
|
|
525
|
+
await runPruneValve("conv-1", { exemptSlugs: new Set() }),
|
|
526
|
+
).toBeNull();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("zero-byte capability rows never trigger the valve (the injector's bytes:0 contract)", async () => {
|
|
530
|
+
// Capability content can never be located/stripped by slug, so the
|
|
531
|
+
// injector records capability slugs at zero bytes — they contribute
|
|
532
|
+
// nothing to the resident measure and are never candidates.
|
|
533
|
+
insertUserRowWithV3Block(
|
|
534
|
+
"conv-1",
|
|
535
|
+
"m1",
|
|
536
|
+
renderCardsBlockInner([card("page-a"), CAPABILITY_CHUNK]),
|
|
537
|
+
);
|
|
538
|
+
recordInjected(
|
|
539
|
+
"conv-1",
|
|
540
|
+
[
|
|
541
|
+
{ slug: "page-a", bytes: 100 },
|
|
542
|
+
{ slug: "skills/meet-join", bytes: 0 },
|
|
543
|
+
],
|
|
544
|
+
1_000,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
pruneConfig = { maxResidentBytes: 300, targetResidentBytes: 200 };
|
|
548
|
+
expect(
|
|
549
|
+
await runPruneValve("conv-1", { exemptSlugs: new Set() }),
|
|
550
|
+
).toBeNull();
|
|
551
|
+
expect(getPrunedSlugs("conv-1").size).toBe(0);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("accounting drift (bytes recorded, no locatable section) is tombstoned in ONE pass — no loop-fire", async () => {
|
|
555
|
+
// Regression: a slug whose recorded bytes have no locatable persisted
|
|
556
|
+
// card section (e.g. its metadata row was lost) pushes the store total
|
|
557
|
+
// over the cap. The valve tombstones it like any candidate — the strip
|
|
558
|
+
// finds nothing to remove, but the tombstone removes its bytes from the
|
|
559
|
+
// resident accounting, so the very next pass is a no-op rather than the
|
|
560
|
+
// valve loop-firing against bytes it cannot free.
|
|
561
|
+
const inner = renderCardsBlockInner([card("page-a")]);
|
|
562
|
+
insertUserRowWithV3Block("conv-1", "m1", inner);
|
|
563
|
+
recordInjected(
|
|
564
|
+
"conv-1",
|
|
565
|
+
[
|
|
566
|
+
{ slug: "page-drifted", bytes: 500 }, // no persisted section anywhere
|
|
567
|
+
{ slug: "page-a", bytes: 100 },
|
|
568
|
+
],
|
|
569
|
+
1_000,
|
|
570
|
+
);
|
|
571
|
+
insertSelection("conv-1", 0, "page-drifted", 1_000);
|
|
572
|
+
insertSelection("conv-1", 0, "page-a", 2_000);
|
|
573
|
+
|
|
574
|
+
const liveMessages: Message[] = [
|
|
575
|
+
{
|
|
576
|
+
role: "user",
|
|
577
|
+
content: [
|
|
578
|
+
{ type: "text", text: wrapMemoryBlock(inner) },
|
|
579
|
+
{ type: "text", text: "turn 1" },
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
// Resident 600 > max 300; tombstoning the drifted slug's 500 bytes
|
|
585
|
+
// reaches the 200 target in one pass without touching page-a.
|
|
586
|
+
pruneConfig = { maxResidentBytes: 300, targetResidentBytes: 200 };
|
|
587
|
+
const plan = await runPruneValve("conv-1", {
|
|
588
|
+
exemptSlugs: new Set(),
|
|
589
|
+
liveMessages: () => liveMessages,
|
|
590
|
+
now: 9_000,
|
|
591
|
+
});
|
|
592
|
+
expect(plan).toEqual({ slugs: ["page-drifted"], bytesFreed: 500 });
|
|
593
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
594
|
+
|
|
595
|
+
// The live history is untouched — the drifted slug had no card section
|
|
596
|
+
// to strip, and page-a was not pruned.
|
|
597
|
+
expect(liveMessages[0]!.content).toEqual([
|
|
598
|
+
{ type: "text", text: wrapMemoryBlock(inner) },
|
|
599
|
+
{ type: "text", text: "turn 1" },
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
// One pass self-heals the accounting: the next run is a no-op (no
|
|
603
|
+
// loop-fire), with nothing further tombstoned.
|
|
604
|
+
expect(
|
|
605
|
+
await runPruneValve("conv-1", {
|
|
606
|
+
exemptSlugs: new Set(),
|
|
607
|
+
liveMessages: () => liveMessages,
|
|
608
|
+
}),
|
|
609
|
+
).toBeNull();
|
|
610
|
+
expect(getPrunedSlugs("conv-1")).toEqual(new Set(["page-drifted"]));
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("over the cap: marks pruned, strips the live history, and converges with rehydration", async () => {
|
|
614
|
+
const innerTurn1 = renderCardsBlockInner([card("page-a"), card("page-b")]);
|
|
615
|
+
const innerTurn2 = renderCardsBlockInner([card("page-c")]);
|
|
616
|
+
insertUserRowWithV3Block("conv-1", "m1", innerTurn1);
|
|
617
|
+
insertUserRowWithV3Block("conv-1", "m2", innerTurn2);
|
|
618
|
+
|
|
619
|
+
recordInjected(
|
|
620
|
+
"conv-1",
|
|
621
|
+
[
|
|
622
|
+
{ slug: "page-a", bytes: 100 },
|
|
623
|
+
{ slug: "page-b", bytes: 100 },
|
|
624
|
+
{ slug: "page-c", bytes: 100 },
|
|
625
|
+
],
|
|
626
|
+
1_000,
|
|
627
|
+
);
|
|
628
|
+
insertSelection("conv-1", 0, "page-a", 1_000);
|
|
629
|
+
insertSelection("conv-1", 0, "page-b", 2_000);
|
|
630
|
+
insertSelection("conv-1", 1, "page-c", 3_000);
|
|
631
|
+
|
|
632
|
+
// Live history as rehydration would build it, plus a v2-lookalike block
|
|
633
|
+
// that must survive untouched.
|
|
634
|
+
const v2Inner = `${V3_CARDS_INJECTION_HEADER}\n\n# memory/concepts/page-a.md\nv2 summary of page a`;
|
|
635
|
+
const liveMessages: Message[] = [
|
|
636
|
+
{
|
|
637
|
+
role: "user",
|
|
638
|
+
content: [
|
|
639
|
+
{ type: "text", text: wrapMemoryBlock(v2Inner) },
|
|
640
|
+
{ type: "text", text: wrapMemoryBlock(innerTurn1) },
|
|
641
|
+
{ type: "text", text: "turn 1" },
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
{ role: "assistant", content: [{ type: "text", text: "reply" }] },
|
|
645
|
+
{
|
|
646
|
+
role: "user",
|
|
647
|
+
content: [
|
|
648
|
+
{ type: "text", text: wrapMemoryBlock(innerTurn2) },
|
|
649
|
+
{ type: "text", text: "turn 2" },
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
pruneConfig = { maxResidentBytes: 250, targetResidentBytes: 100 };
|
|
655
|
+
const plan = await runPruneValve("conv-1", {
|
|
656
|
+
exemptSlugs: new Set(),
|
|
657
|
+
liveMessages: () => liveMessages,
|
|
658
|
+
now: 9_000,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
expect(plan).toEqual({ slugs: ["page-a", "page-b"], bytesFreed: 200 });
|
|
662
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-c"]));
|
|
663
|
+
expect(getInjected("conv-1").get("page-a")!.prunedAt).toBe(9_000);
|
|
664
|
+
|
|
665
|
+
// Turn-1's v3 block lost BOTH pruned cards → removed outright; the
|
|
666
|
+
// v2-lookalike and turn-2's block are byte-identical.
|
|
667
|
+
expect(liveMessages[0]!.content).toEqual([
|
|
668
|
+
{ type: "text", text: wrapMemoryBlock(v2Inner) },
|
|
669
|
+
{ type: "text", text: "turn 1" },
|
|
670
|
+
]);
|
|
671
|
+
expect(liveMessages[2]!.content).toEqual([
|
|
672
|
+
{ type: "text", text: wrapMemoryBlock(innerTurn2) },
|
|
673
|
+
{ type: "text", text: "turn 2" },
|
|
674
|
+
]);
|
|
675
|
+
|
|
676
|
+
// Rehydration converges: the same filter over the persisted metadata
|
|
677
|
+
// produces exactly what the live strip left in place.
|
|
678
|
+
const pruned = getPrunedSlugs("conv-1");
|
|
679
|
+
expect(filterPrunedCardSections(innerTurn1, pruned)).toBe("");
|
|
680
|
+
expect(filterPrunedCardSections(innerTurn2, pruned)).toBe(innerTurn2);
|
|
681
|
+
|
|
682
|
+
// Idempotent: resident is at 100 ≤ target → next pass is a no-op.
|
|
683
|
+
expect(
|
|
684
|
+
await runPruneValve("conv-1", { exemptSlugs: new Set() }),
|
|
685
|
+
).toBeNull();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("a pruned page later re-selected re-injects and is kept by the filter again", async () => {
|
|
689
|
+
const inner = renderCardsBlockInner([card("page-a")]);
|
|
690
|
+
insertUserRowWithV3Block("conv-1", "m1", inner);
|
|
691
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 300 }], 1_000);
|
|
692
|
+
|
|
693
|
+
pruneConfig = { maxResidentBytes: 200, targetResidentBytes: 100 };
|
|
694
|
+
const plan = await runPruneValve("conv-1", {
|
|
695
|
+
exemptSlugs: new Set(),
|
|
696
|
+
liveMessages: () => null,
|
|
697
|
+
now: 2_000,
|
|
698
|
+
});
|
|
699
|
+
expect(plan!.slugs).toEqual(["page-a"]);
|
|
700
|
+
expect(filterPrunedCardSections(inner, getPrunedSlugs("conv-1"))).toBe("");
|
|
701
|
+
|
|
702
|
+
// Re-selection re-injects (PR 4 contract: recordInjected clears
|
|
703
|
+
// pruned_at) — the slug is active again and the filter keeps its card.
|
|
704
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 50 }], 3_000);
|
|
705
|
+
expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
706
|
+
expect(filterPrunedCardSections(inner, getPrunedSlugs("conv-1"))).toBe(
|
|
707
|
+
inner,
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe("schedulePruneValve", () => {
|
|
713
|
+
test("defers the valve run; flush awaits completion", async () => {
|
|
714
|
+
const inner = renderCardsBlockInner([card("page-a"), card("page-b")]);
|
|
715
|
+
insertUserRowWithV3Block("conv-1", "m1", inner);
|
|
716
|
+
recordInjected(
|
|
717
|
+
"conv-1",
|
|
718
|
+
[
|
|
719
|
+
{ slug: "page-a", bytes: 200 },
|
|
720
|
+
{ slug: "page-b", bytes: 200 },
|
|
721
|
+
],
|
|
722
|
+
1_000,
|
|
723
|
+
);
|
|
724
|
+
insertSelection("conv-1", 0, "page-a", 1_000);
|
|
725
|
+
insertSelection("conv-1", 0, "page-b", 2_000);
|
|
726
|
+
|
|
727
|
+
pruneConfig = { maxResidentBytes: 300, targetResidentBytes: 200 };
|
|
728
|
+
schedulePruneValve("conv-1", {
|
|
729
|
+
exemptSlugs: new Set(),
|
|
730
|
+
liveMessages: () => null,
|
|
731
|
+
});
|
|
732
|
+
// Synchronously after scheduling, nothing has been pruned yet.
|
|
733
|
+
expect(getPrunedSlugs("conv-1").size).toBe(0);
|
|
734
|
+
|
|
735
|
+
await flushPruneValveForTests();
|
|
736
|
+
expect(getPrunedSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("valve failures are swallowed (never affect the turn)", async () => {
|
|
740
|
+
pruneConfig = { maxResidentBytes: 0, targetResidentBytes: 0 };
|
|
741
|
+
insertUserRowWithV3Block(
|
|
742
|
+
"conv-1",
|
|
743
|
+
"m1",
|
|
744
|
+
renderCardsBlockInner([card("page-a")]),
|
|
745
|
+
);
|
|
746
|
+
recordInjected("conv-1", [{ slug: "page-a", bytes: 100 }], 1_000);
|
|
747
|
+
schedulePruneValve("conv-1", {
|
|
748
|
+
exemptSlugs: new Set(),
|
|
749
|
+
liveMessages: () => {
|
|
750
|
+
throw new Error("boom");
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
await flushPruneValveForTests();
|
|
754
|
+
// The markPruned preceding the failing strip still landed; the error
|
|
755
|
+
// itself did not propagate.
|
|
756
|
+
expect(getPrunedSlugs("conv-1")).toEqual(new Set(["page-a"]));
|
|
757
|
+
});
|
|
758
|
+
});
|