@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,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-v3 prune valve: a structural bound on the resident frozen-card
|
|
3
|
+
* footprint.
|
|
4
|
+
*
|
|
5
|
+
* Frozen cards accumulate in history with no per-turn bound (the injector
|
|
6
|
+
* renders net-new only and never strips prior blocks — the cache contract).
|
|
7
|
+
* The valve is the backstop: when the resident (non-pruned) card bytes exceed
|
|
8
|
+
* `memory.v3.prune.maxResidentBytes`, the least-recently-selected
|
|
9
|
+
* non-core/non-hot cards are pruned, oldest first, until the footprint is at
|
|
10
|
+
* `targetResidentBytes`.
|
|
11
|
+
*
|
|
12
|
+
* Pruning is `markPruned` (the store's audit-preserving tombstone) plus two
|
|
13
|
+
* FILTER points — never a metadata rewrite, so the persisted
|
|
14
|
+
* `metadata.memoryV3InjectedBlock` rows stay intact (auditable, and a
|
|
15
|
+
* re-selected slug re-injects as a fresh card because `recordInjected` clears
|
|
16
|
+
* `pruned_at`):
|
|
17
|
+
*
|
|
18
|
+
* (a) a one-time strip of the pruned cards' sections from the `<memory>`
|
|
19
|
+
* blocks riding the LIVE in-memory history
|
|
20
|
+
* ({@link stripPrunedCardsFromMessages} — per-card boundaries are the
|
|
21
|
+
* `# memory/concepts/<slug>.md` headers within a block, terminated at
|
|
22
|
+
* any other top-level header chunk such as capability content; see
|
|
23
|
+
* {@link parseCardSections}). The strip mutates the shared message
|
|
24
|
+
* objects in place so the agent loop's end-of-turn history fold-back
|
|
25
|
+
* keeps the stripped content;
|
|
26
|
+
* (b) the `loadFromDb` rehydration splice in `daemon/conversation.ts`
|
|
27
|
+
* re-applies {@link filterPrunedCardSections} on every load, so prunes
|
|
28
|
+
* persist across daemon restarts without touching the metadata.
|
|
29
|
+
*
|
|
30
|
+
* The first post-prune request loses the provider prefix cache from the
|
|
31
|
+
* earliest affected message — ONE amortized bust per prune, logged with
|
|
32
|
+
* `prunedSlugs` / `bytesFreed`.
|
|
33
|
+
*
|
|
34
|
+
* v2-coexistence note: v2's dynamic `<memory>` blocks share the exact wrapper
|
|
35
|
+
* and `# memory/concepts/<slug>.md` header convention, so the live strip
|
|
36
|
+
* cannot tell layers apart syntactically. A block is treated as v3-owned only
|
|
37
|
+
* when EVERY card section in it byte-matches a section of some persisted
|
|
38
|
+
* `memoryV3InjectedBlock` for the conversation
|
|
39
|
+
* ({@link collectPersistedV3Cards}) — v2 sections render the page SUMMARY
|
|
40
|
+
* (or full page) rather than the card head+TOC, so pre-flip v2 blocks never
|
|
41
|
+
* qualify and are left untouched, keeping their unfiltered rehydration
|
|
42
|
+
* byte-identical.
|
|
43
|
+
*
|
|
44
|
+
* Capability note: skill / CLI-command content renders under its own
|
|
45
|
+
* `# Skill:` / `# CLI command:` header — not a card section — so it can never
|
|
46
|
+
* be located (and therefore never stripped) by slug. The injector records
|
|
47
|
+
* capability slugs at `bytes: 0`, which keeps them out of the resident
|
|
48
|
+
* measure AND out of candidacy (zero-byte rows are skipped — pruning them
|
|
49
|
+
* frees nothing); capability content riding a card block survives the prune
|
|
50
|
+
* of its neighboring cards as a non-card chunk.
|
|
51
|
+
*
|
|
52
|
+
* Accounting-drift note: a slug whose recorded bytes have no locatable
|
|
53
|
+
* persisted card section (e.g. its metadata row was lost) can be planned and
|
|
54
|
+
* tombstoned — the strip/rehydration filter simply finds nothing to remove,
|
|
55
|
+
* its content (if any) stays in context, and its bytes leave the resident
|
|
56
|
+
* accounting with the tombstone. That self-heals in ONE pass: the next valve
|
|
57
|
+
* run measures the corrected footprint, so the valve never loop-fires against
|
|
58
|
+
* bytes it cannot free.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import { getConfig } from "../../../config/loader.js";
|
|
62
|
+
import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
|
|
63
|
+
import {
|
|
64
|
+
unwrapMemoryBlock,
|
|
65
|
+
wrapMemoryBlock,
|
|
66
|
+
} from "../../../memory/memory-marker.js";
|
|
67
|
+
import {
|
|
68
|
+
INJECTED_CONCEPT_HEADER_REGEX,
|
|
69
|
+
readInjectedBlock,
|
|
70
|
+
} from "../../../memory/v2/injected-block-slugs.js";
|
|
71
|
+
import type { ContentBlock, Message } from "../../../providers/types.js";
|
|
72
|
+
import { getLogger } from "../../../util/logger.js";
|
|
73
|
+
import {
|
|
74
|
+
getActiveEntries,
|
|
75
|
+
getPrunedSlugs,
|
|
76
|
+
markPruned,
|
|
77
|
+
MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
|
|
78
|
+
residentBytes,
|
|
79
|
+
} from "./ever-injected-store.js";
|
|
80
|
+
|
|
81
|
+
const log = getLogger("memory-v3-shadow");
|
|
82
|
+
|
|
83
|
+
// ─── card-section parsing & filtering ────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/** Matches any top-level `# ` header line — concept card headers AND foreign
|
|
86
|
+
* headers like a capability chunk's `# Skill:` / `# CLI command:` line. */
|
|
87
|
+
const TOP_LEVEL_HEADER_REGEX = /^# /gm;
|
|
88
|
+
|
|
89
|
+
/** One parsed card section: the header line plus everything up to the next
|
|
90
|
+
* chunk boundary (or end of block), trailing whitespace removed. */
|
|
91
|
+
export interface CardSection {
|
|
92
|
+
slug: string;
|
|
93
|
+
/** The section text INCLUDING its `# memory/concepts/<slug>.md` header
|
|
94
|
+
* line, `trimEnd()`ed so re-joining with `\n\n` reproduces the renderer's
|
|
95
|
+
* exact bytes. */
|
|
96
|
+
text: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One ordered chunk of a parsed card block: a concept card (prunable, owned
|
|
100
|
+
* by `slug`) or any other `\n\n`-joined chunk (e.g. capability content under
|
|
101
|
+
* its own `# Skill:` / `# CLI command:` header — never prunable). */
|
|
102
|
+
export type CardBlockPiece =
|
|
103
|
+
| { kind: "card"; slug: string; text: string }
|
|
104
|
+
| { kind: "other"; text: string };
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Split an UNWRAPPED card-block body into its preamble (the instruction
|
|
108
|
+
* header — everything before the first boundary), the ordered chunk pieces,
|
|
109
|
+
* and the card sections (the `kind: "card"` pieces, kept as a convenience
|
|
110
|
+
* view). Returns zero sections/pieces when the text carries no concept card
|
|
111
|
+
* headers.
|
|
112
|
+
*
|
|
113
|
+
* A card's section ends at the next concept header OR at any other top-level
|
|
114
|
+
* `# ` header that starts its own `\n\n`-joined chunk — so a capability chunk
|
|
115
|
+
* trailing a concept card (`renderCardsBlockInner` joins them with `\n\n`) is
|
|
116
|
+
* parsed as a separate non-card piece instead of being absorbed into the
|
|
117
|
+
* card, and pruning the card never deletes it. The blank-line requirement
|
|
118
|
+
* keeps a card head's own `# Title` line (which follows the path header with
|
|
119
|
+
* a single `\n`) from splitting the card, and guarantees splits land on the
|
|
120
|
+
* renderer's `\n\n` seams so re-joins stay byte-identical.
|
|
121
|
+
*/
|
|
122
|
+
export function parseCardSections(inner: string): {
|
|
123
|
+
preamble: string;
|
|
124
|
+
sections: CardSection[];
|
|
125
|
+
pieces: CardBlockPiece[];
|
|
126
|
+
} {
|
|
127
|
+
const cardMatches = [...inner.matchAll(INJECTED_CONCEPT_HEADER_REGEX)];
|
|
128
|
+
if (cardMatches.length === 0) {
|
|
129
|
+
return { preamble: inner, sections: [], pieces: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const cardStarts = new Set(cardMatches.map((match) => match.index!));
|
|
133
|
+
const boundaries: Array<{ index: number; slug: string | null }> =
|
|
134
|
+
cardMatches.map((match) => ({ index: match.index!, slug: match[1]! }));
|
|
135
|
+
for (const match of inner.matchAll(TOP_LEVEL_HEADER_REGEX)) {
|
|
136
|
+
const i = match.index!;
|
|
137
|
+
if (cardStarts.has(i)) continue;
|
|
138
|
+
// Foreign header on a `\n\n` seam → starts its own chunk.
|
|
139
|
+
if (i >= 2 && inner[i - 1] === "\n" && inner[i - 2] === "\n") {
|
|
140
|
+
boundaries.push({ index: i, slug: null });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
boundaries.sort((a, b) => a.index - b.index);
|
|
144
|
+
|
|
145
|
+
const preamble = inner.slice(0, boundaries[0]!.index).trimEnd();
|
|
146
|
+
const pieces = boundaries.map((boundary, i): CardBlockPiece => {
|
|
147
|
+
const end =
|
|
148
|
+
i + 1 < boundaries.length ? boundaries[i + 1]!.index : undefined;
|
|
149
|
+
const text = inner.slice(boundary.index, end).trimEnd();
|
|
150
|
+
return boundary.slug === null
|
|
151
|
+
? { kind: "other", text }
|
|
152
|
+
: { kind: "card", slug: boundary.slug, text };
|
|
153
|
+
});
|
|
154
|
+
const sections = pieces.filter(
|
|
155
|
+
(piece): piece is Extract<CardBlockPiece, { kind: "card" }> =>
|
|
156
|
+
piece.kind === "card",
|
|
157
|
+
);
|
|
158
|
+
return { preamble, sections, pieces };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Remove pruned slugs' card sections from an unwrapped block body.
|
|
163
|
+
*
|
|
164
|
+
* Returns the input string UNCHANGED (same reference) when nothing is
|
|
165
|
+
* removed — callers use identity to detect a no-op — and `""` when every
|
|
166
|
+
* chunk is pruned (the caller drops/skips the whole block; a bare
|
|
167
|
+
* instruction header with no cards carries no content). Non-card chunks
|
|
168
|
+
* (capability content) are always kept, so a block whose concept cards are
|
|
169
|
+
* all pruned but which carries capability content keeps its preamble and
|
|
170
|
+
* that content. Kept chunks are re-joined exactly as the renderer joined
|
|
171
|
+
* them (`\n\n`), so an unpruned remainder stays byte-identical to what a
|
|
172
|
+
* fresh render of those chunks would produce.
|
|
173
|
+
*/
|
|
174
|
+
export function filterPrunedCardSections(
|
|
175
|
+
inner: string,
|
|
176
|
+
prunedSlugs: ReadonlySet<string>,
|
|
177
|
+
): string {
|
|
178
|
+
const { preamble, sections, pieces } = parseCardSections(inner);
|
|
179
|
+
if (sections.length === 0) return inner;
|
|
180
|
+
|
|
181
|
+
const kept = pieces.filter(
|
|
182
|
+
(piece) => piece.kind !== "card" || !prunedSlugs.has(piece.slug),
|
|
183
|
+
);
|
|
184
|
+
if (kept.length === pieces.length) return inner;
|
|
185
|
+
if (kept.length === 0) return "";
|
|
186
|
+
|
|
187
|
+
const texts = kept.map((piece) => piece.text);
|
|
188
|
+
if (preamble.length > 0) texts.unshift(preamble);
|
|
189
|
+
return texts.join("\n\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── prune planning ──────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export interface PrunePlan {
|
|
195
|
+
/** Slugs to prune, least-recently-selected first. */
|
|
196
|
+
slugs: string[];
|
|
197
|
+
/** Resident bytes the plan frees once executed. */
|
|
198
|
+
bytesFreed: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface PruneDeps {
|
|
202
|
+
maxResidentBytes: number;
|
|
203
|
+
targetResidentBytes: number;
|
|
204
|
+
/** Core + hot lane members — the selector's stable prefix must never be
|
|
205
|
+
* pruned out from under it. */
|
|
206
|
+
exemptSlugs: ReadonlySet<string>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Plan a prune for the conversation, or `null` when the resident footprint is
|
|
211
|
+
* within `maxResidentBytes` (or nothing is prunable).
|
|
212
|
+
*
|
|
213
|
+
* The footprint and the candidates both range over the ACTIVE injected slugs.
|
|
214
|
+
* Candidates are ranked by last selection recency — `MAX(created_at)` per
|
|
215
|
+
* slug from `memory_v3_selections`, falling back to the store's `injected_at`
|
|
216
|
+
* for slugs with no selection rows (e.g. rows copied by a full fork) — taken
|
|
217
|
+
* oldest-first until the footprint is at `targetResidentBytes`. Core/hot lane
|
|
218
|
+
* members are exempt, and zero-byte rows (capability slugs, truncated-fork
|
|
219
|
+
* seeds: dedup-only, no byte accounting) are skipped — pruning them frees
|
|
220
|
+
* nothing while discarding inherited context.
|
|
221
|
+
*/
|
|
222
|
+
export function planPrune(
|
|
223
|
+
deps: PruneDeps,
|
|
224
|
+
conversationId: string,
|
|
225
|
+
): PrunePlan | null {
|
|
226
|
+
const activeEntries = getActiveEntries(conversationId);
|
|
227
|
+
const resident = activeEntries.reduce((sum, e) => sum + e.bytes, 0);
|
|
228
|
+
if (resident <= deps.maxResidentBytes) return null;
|
|
229
|
+
|
|
230
|
+
const selectionRows = getSqliteFrom(getDb())
|
|
231
|
+
.query(
|
|
232
|
+
/*sql*/ `
|
|
233
|
+
SELECT slug, MAX(created_at) AS lastSelectedAt FROM memory_v3_selections
|
|
234
|
+
WHERE conversation_id = ?
|
|
235
|
+
GROUP BY slug
|
|
236
|
+
`,
|
|
237
|
+
)
|
|
238
|
+
.all(conversationId) as Array<{ slug: string; lastSelectedAt: number }>;
|
|
239
|
+
const lastSelectedAt = new Map(
|
|
240
|
+
selectionRows.map((row) => [row.slug, row.lastSelectedAt]),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const candidates = activeEntries
|
|
244
|
+
.filter((entry) => entry.bytes > 0 && !deps.exemptSlugs.has(entry.slug))
|
|
245
|
+
.map((entry) => ({
|
|
246
|
+
...entry,
|
|
247
|
+
recency: lastSelectedAt.get(entry.slug) ?? entry.injectedAt,
|
|
248
|
+
}))
|
|
249
|
+
// Oldest first; slug ascending as the deterministic tiebreak.
|
|
250
|
+
.sort((a, b) => a.recency - b.recency || (a.slug < b.slug ? -1 : 1));
|
|
251
|
+
|
|
252
|
+
const slugs: string[] = [];
|
|
253
|
+
let bytesFreed = 0;
|
|
254
|
+
for (const candidate of candidates) {
|
|
255
|
+
if (resident - bytesFreed <= deps.targetResidentBytes) break;
|
|
256
|
+
slugs.push(candidate.slug);
|
|
257
|
+
bytesFreed += candidate.bytes;
|
|
258
|
+
}
|
|
259
|
+
return slugs.length === 0 ? null : { slugs, bytesFreed };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── live-history strip ──────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Collect the conversation's known v3 card SECTION TEXTS from every persisted
|
|
266
|
+
* `metadata.memoryV3InjectedBlock` row — the live strip's v3-ownership test:
|
|
267
|
+
* a live `<memory>` block is v3-owned iff all of its card sections appear
|
|
268
|
+
* here (see the module doc's v2-coexistence note). Capability chunks never
|
|
269
|
+
* contribute: their content renders under `# Skill:` / `# CLI command:`
|
|
270
|
+
* headers, which parse as non-card chunks.
|
|
271
|
+
*/
|
|
272
|
+
export function collectPersistedV3Cards(conversationId: string): Set<string> {
|
|
273
|
+
// Substring prefilter (indexable LIKE) mirrors the Slack metadata scan;
|
|
274
|
+
// rows are validated by `readInjectedBlock`'s JSON parse.
|
|
275
|
+
const rows = getSqliteFrom(getDb())
|
|
276
|
+
.query(
|
|
277
|
+
/*sql*/ `
|
|
278
|
+
SELECT metadata FROM messages
|
|
279
|
+
WHERE conversation_id = ? AND metadata LIKE '%' || ? || '%'
|
|
280
|
+
`,
|
|
281
|
+
)
|
|
282
|
+
.all(conversationId, MEMORY_V3_INJECTED_BLOCK_METADATA_KEY) as Array<{
|
|
283
|
+
metadata: string | null;
|
|
284
|
+
}>;
|
|
285
|
+
|
|
286
|
+
const sections = new Set<string>();
|
|
287
|
+
for (const row of rows) {
|
|
288
|
+
const block = readInjectedBlock(
|
|
289
|
+
row.metadata,
|
|
290
|
+
MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
|
|
291
|
+
);
|
|
292
|
+
if (block === null) continue;
|
|
293
|
+
for (const section of parseCardSections(unwrapMemoryBlock(block))
|
|
294
|
+
.sections) {
|
|
295
|
+
sections.add(section.text);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return sections;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* One-time strip of pruned cards from the live in-memory history: for every
|
|
303
|
+
* v3-owned `<memory>` text block (ownership per `knownV3Sections` — see the
|
|
304
|
+
* module doc), drop the pruned slugs' card sections; a block whose cards are
|
|
305
|
+
* all pruned is removed outright (matching the rehydration splice, which
|
|
306
|
+
* skips an all-pruned block).
|
|
307
|
+
*
|
|
308
|
+
* Mutates the affected `Message` objects IN PLACE (`message.content`
|
|
309
|
+
* reassignment): the agent loop's working arrays share these object
|
|
310
|
+
* references, so its end-of-turn history fold-back keeps the strip. Returns
|
|
311
|
+
* the number of blocks changed.
|
|
312
|
+
*/
|
|
313
|
+
export function stripPrunedCardsFromMessages(
|
|
314
|
+
messages: Message[],
|
|
315
|
+
prunedSlugs: ReadonlySet<string>,
|
|
316
|
+
knownV3Sections: ReadonlySet<string>,
|
|
317
|
+
): number {
|
|
318
|
+
let strippedBlocks = 0;
|
|
319
|
+
for (const message of messages) {
|
|
320
|
+
if (message.role !== "user") continue;
|
|
321
|
+
let changed = false;
|
|
322
|
+
const nextContent: ContentBlock[] = [];
|
|
323
|
+
for (const block of message.content) {
|
|
324
|
+
if (block.type !== "text") {
|
|
325
|
+
nextContent.push(block);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const inner = unwrapMemoryBlock(block.text);
|
|
329
|
+
if (inner === block.text) {
|
|
330
|
+
// Not a wrapped `<memory>` block (unwrap is identity on anything
|
|
331
|
+
// without the full wrapper pair).
|
|
332
|
+
nextContent.push(block);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const { sections } = parseCardSections(inner);
|
|
336
|
+
const isV3Block =
|
|
337
|
+
sections.length > 0 &&
|
|
338
|
+
sections.every((section) => knownV3Sections.has(section.text));
|
|
339
|
+
if (!isV3Block) {
|
|
340
|
+
nextContent.push(block);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const filtered = filterPrunedCardSections(inner, prunedSlugs);
|
|
344
|
+
if (filtered === inner) {
|
|
345
|
+
nextContent.push(block);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
strippedBlocks += 1;
|
|
349
|
+
changed = true;
|
|
350
|
+
if (filtered.length > 0) {
|
|
351
|
+
nextContent.push({ type: "text", text: wrapMemoryBlock(filtered) });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (changed) message.content = nextContent;
|
|
355
|
+
}
|
|
356
|
+
return strippedBlocks;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── valve execution & trigger ───────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export interface PruneValveOptions {
|
|
362
|
+
/** Core + hot lane members (never pruned). */
|
|
363
|
+
exemptSlugs: ReadonlySet<string>;
|
|
364
|
+
/** Test seam: resolve the conversation's LIVE in-memory message array.
|
|
365
|
+
* Defaults to the daemon conversation registry (dynamically imported — a
|
|
366
|
+
* static import would cycle with `daemon/conversation.ts`, which imports
|
|
367
|
+
* this module for the rehydration filter). `null` skips the live strip
|
|
368
|
+
* (the rehydration filter still applies the prune on next load). */
|
|
369
|
+
liveMessages?: (conversationId: string) => Message[] | null;
|
|
370
|
+
/** Test seam for the `pruned_at` timestamp. */
|
|
371
|
+
now?: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function defaultLiveMessages(
|
|
375
|
+
conversationId: string,
|
|
376
|
+
): Promise<Message[] | null> {
|
|
377
|
+
const { findConversationOrSubagent } =
|
|
378
|
+
await import("../../../daemon/conversation-registry.js");
|
|
379
|
+
return findConversationOrSubagent(conversationId)?.messages ?? null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Run the prune valve once: plan against `memory.v3.prune` config, mark the
|
|
384
|
+
* planned slugs pruned, and strip their cards from the live in-memory
|
|
385
|
+
* history. Returns the executed plan, or `null` when the footprint is within
|
|
386
|
+
* bounds (the common case — repeated invocations below the cap are no-ops).
|
|
387
|
+
*
|
|
388
|
+
* The live strip filters with the conversation's FULL pruned set (not just
|
|
389
|
+
* this plan's slugs), so a card an earlier strip could not reach — e.g. a
|
|
390
|
+
* block not yet folded back into the live history when that prune ran —
|
|
391
|
+
* self-heals on the next prune.
|
|
392
|
+
*/
|
|
393
|
+
export async function runPruneValve(
|
|
394
|
+
conversationId: string,
|
|
395
|
+
options: PruneValveOptions,
|
|
396
|
+
): Promise<PrunePlan | null> {
|
|
397
|
+
// Defensive read: test configs may omit the prune block entirely.
|
|
398
|
+
const pruneConfig = getConfig().memory?.v3?.prune;
|
|
399
|
+
if (!pruneConfig) return null;
|
|
400
|
+
|
|
401
|
+
// Planning needs only the store (cheap); the persisted-metadata scan for
|
|
402
|
+
// the live strip's ownership test runs only once a plan exists — a
|
|
403
|
+
// conversation within the cap never pays it (the common case).
|
|
404
|
+
const plan = planPrune(
|
|
405
|
+
{
|
|
406
|
+
maxResidentBytes: pruneConfig.maxResidentBytes,
|
|
407
|
+
targetResidentBytes: pruneConfig.targetResidentBytes,
|
|
408
|
+
exemptSlugs: options.exemptSlugs,
|
|
409
|
+
},
|
|
410
|
+
conversationId,
|
|
411
|
+
);
|
|
412
|
+
if (!plan) return null;
|
|
413
|
+
|
|
414
|
+
markPruned(conversationId, plan.slugs, options.now ?? Date.now());
|
|
415
|
+
|
|
416
|
+
const liveMessages = options.liveMessages
|
|
417
|
+
? options.liveMessages(conversationId)
|
|
418
|
+
: await defaultLiveMessages(conversationId);
|
|
419
|
+
let strippedBlocks = 0;
|
|
420
|
+
if (liveMessages) {
|
|
421
|
+
strippedBlocks = stripPrunedCardsFromMessages(
|
|
422
|
+
liveMessages,
|
|
423
|
+
getPrunedSlugs(conversationId),
|
|
424
|
+
collectPersistedV3Cards(conversationId),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
log.info(
|
|
429
|
+
{
|
|
430
|
+
conversationId,
|
|
431
|
+
prunedSlugs: plan.slugs.length,
|
|
432
|
+
bytesFreed: plan.bytesFreed,
|
|
433
|
+
strippedBlocks,
|
|
434
|
+
residentBytes: residentBytes(conversationId),
|
|
435
|
+
},
|
|
436
|
+
"memory-v3 prune valve: pruned least-recently-selected cards (one amortized prefix-cache bust)",
|
|
437
|
+
);
|
|
438
|
+
return plan;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Pending valve work, chained so runs serialize per process and tests can
|
|
442
|
+
* await completion ({@link flushPruneValveForTests}). */
|
|
443
|
+
let pendingPrune: Promise<unknown> = Promise.resolve();
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* End-of-turn trigger: defer a {@link runPruneValve} pass so prune work never
|
|
447
|
+
* delays the in-flight turn's assembly. Failures are logged and swallowed —
|
|
448
|
+
* the valve must never affect the live turn.
|
|
449
|
+
*/
|
|
450
|
+
export function schedulePruneValve(
|
|
451
|
+
conversationId: string,
|
|
452
|
+
options: PruneValveOptions,
|
|
453
|
+
): void {
|
|
454
|
+
pendingPrune = pendingPrune
|
|
455
|
+
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
|
|
456
|
+
.then(() => runPruneValve(conversationId, options))
|
|
457
|
+
.catch((err) => {
|
|
458
|
+
log.warn(
|
|
459
|
+
{
|
|
460
|
+
err: err instanceof Error ? err.message : String(err),
|
|
461
|
+
conversationId,
|
|
462
|
+
},
|
|
463
|
+
"memory-v3 prune valve failed (non-fatal)",
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Await all scheduled valve work (deterministic teardown for tests). */
|
|
469
|
+
export function flushPruneValveForTests(): Promise<void> {
|
|
470
|
+
return pendingPrune.then(() => undefined);
|
|
471
|
+
}
|
|
@@ -2,38 +2,90 @@ import { wrapMemoryBlock } from "../../../memory/memory-marker.js";
|
|
|
2
2
|
import { Section, Slug } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Leading instruction line of the frozen card block — byte-identical to v2's
|
|
6
|
+
* `INJECTION_HEADER` (`memory/v2/injection.ts`) so the read affordance the
|
|
7
|
+
* model already knows applies to cards unchanged: every card carries a
|
|
8
|
+
* `# memory/concepts/<slug>.md` path header it can `file_read`.
|
|
9
|
+
*/
|
|
10
|
+
export const V3_CARDS_INJECTION_HEADER =
|
|
11
|
+
'Use `file_read("memory/concepts/path/to/file.md")` to read the full pages for any of the injected memory summaries you want more information on.';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render the UNWRAPPED inner text of a frozen net-new card block: the v2-style
|
|
15
|
+
* instruction header followed by the rendered cards, blank-line separated.
|
|
16
|
+
* Returns `""` for an empty card list (the injector attaches no block and the
|
|
17
|
+
* caller persists nothing). The caller wraps the result via
|
|
18
|
+
* {@link wrapMemoryBlock} exactly once at injection time and persists the
|
|
19
|
+
* unwrapped form to message metadata — the same wrap-on-use contract as v2's
|
|
20
|
+
* `memoryInjectedBlock`.
|
|
21
|
+
*/
|
|
22
|
+
export function renderCardsBlockInner(cards: string[]): string {
|
|
23
|
+
if (cards.length === 0) return "";
|
|
24
|
+
return [V3_CARDS_INJECTION_HEADER, ...cards].join("\n\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One ephemeral spotlight entry: a selected finder hit's matched section.
|
|
29
|
+
* `title` is the matched section's heading; `text` its indexed section text.
|
|
30
|
+
*/
|
|
31
|
+
export interface SpotlightEntry {
|
|
32
|
+
slug: Slug;
|
|
33
|
+
title: string;
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render the UNWRAPPED inner text of the ephemeral `<memory_spotlight>` block:
|
|
39
|
+
* one `## memory/concepts/<slug>.md § <section heading>` header plus section
|
|
40
|
+
* text per entry, blank-line separated. Returns `""` for an empty window (the
|
|
41
|
+
* injector attaches no block). The caller wraps via
|
|
42
|
+
* `wrapMemorySpotlightBlock`.
|
|
43
|
+
*/
|
|
44
|
+
export function renderSpotlightInner(entries: SpotlightEntry[]): string {
|
|
45
|
+
return entries
|
|
46
|
+
.map(
|
|
47
|
+
(entry) =>
|
|
48
|
+
`## memory/concepts/${entry.slug}.md § ${entry.title}\n${entry.text}`,
|
|
49
|
+
)
|
|
50
|
+
.join("\n\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render a v3 selection set into a single `<memory>…</memory>` text block,
|
|
55
|
+
* injecting each slug's matched section (progressive disclosure) rather than
|
|
56
|
+
* its full page body.
|
|
57
|
+
*
|
|
58
|
+
* INSPECTOR-ONLY: live injection no longer renders the whole selection set —
|
|
59
|
+
* the injector freezes net-new CARDS into history ({@link renderCardsBlockInner})
|
|
60
|
+
* and re-renders the ephemeral spotlight separately. This whole-set render
|
|
61
|
+
* remains for the inspector's selection log (`selection-log-store.ts`), which
|
|
62
|
+
* reconstructs an approximate view of a turn's selection after the fact.
|
|
8
63
|
*
|
|
9
64
|
* Pure and side-effect-free: all I/O is delegated to the injected
|
|
10
65
|
* `pageContent` resolver, which is awaited once per slug in input order with
|
|
11
66
|
* that slug's matched section (`sectionBySlug.get(slug)`, possibly undefined).
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `conversation-graph-memory.ts` already recognizes (and evicts) it.
|
|
15
|
-
*
|
|
16
|
-
* Resolution is concurrent; the rendered block preserves `finalInjection`
|
|
17
|
-
* order regardless of which fetch settles first.
|
|
67
|
+
* Resolution is concurrent; the rendered block preserves `slugs` order
|
|
68
|
+
* regardless of which fetch settles first.
|
|
18
69
|
*
|
|
19
|
-
* @param
|
|
20
|
-
*
|
|
70
|
+
* @param slugs - The selected slugs for the turn, in the order they should
|
|
71
|
+
* appear in the rendered block.
|
|
21
72
|
* @param sectionBySlug - The matched `Section` per slug; a slug with no entry
|
|
22
|
-
* (e.g.
|
|
73
|
+
* (e.g. an edge-only or stable-prefix page with no current match) renders its
|
|
74
|
+
* full/lead page.
|
|
23
75
|
* @param pageContent - Resolver returning a slug's rendered content given its
|
|
24
76
|
* matched section (or undefined).
|
|
25
77
|
* @returns The wrapped `<memory>` block, or `""` for an empty selection (the
|
|
26
|
-
* caller
|
|
78
|
+
* caller renders nothing).
|
|
27
79
|
*/
|
|
28
80
|
export async function renderMemoryBlock(
|
|
29
|
-
|
|
81
|
+
slugs: Slug[],
|
|
30
82
|
sectionBySlug: Map<Slug, Section>,
|
|
31
83
|
pageContent: (slug: Slug, section: Section | undefined) => Promise<string>,
|
|
32
84
|
): Promise<string> {
|
|
33
|
-
if (
|
|
85
|
+
if (slugs.length === 0) return "";
|
|
34
86
|
|
|
35
87
|
const contents = await Promise.all(
|
|
36
|
-
|
|
88
|
+
slugs.map((slug) => pageContent(slug, sectionBySlug.get(slug))),
|
|
37
89
|
);
|
|
38
90
|
|
|
39
91
|
return wrapMemoryBlock(contents.join("\n"));
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-side store for the inspector's Memory V3 section. Reads the persisted
|
|
3
|
-
* `memory_v3_selections` rows for a turn and re-renders
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* `memory_v3_selections` rows for a turn and re-renders an APPROXIMATE
|
|
4
|
+
* `<memory>` block for what v3 selected, so the inspector can show the turn's
|
|
5
|
+
* selection without re-running orchestration — which would be wrong anyway,
|
|
6
|
+
* since the hot lane is frecency-stateful and can't be reproduced after the
|
|
7
|
+
* fact.
|
|
8
|
+
*
|
|
9
|
+
* The rendered text is inspector-only and NOT byte-identical to live
|
|
10
|
+
* injection: the live injector freezes net-new compact CARDS into history
|
|
11
|
+
* (`renderV3CardContent`) plus an ephemeral spotlight, while this store
|
|
12
|
+
* re-renders the whole selection set as full/lead pages
|
|
13
|
+
* (`renderV3SectionContent` with no persisted section identity — see the
|
|
14
|
+
* inline note at `injectedText`).
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
import type { MemoryV3SelectionLog } from "../../../api/responses/memory-v3-selection-log.js";
|
|
@@ -57,9 +64,8 @@ function rowsForTurn(conversationId: string, turn: number): SelectionRow[] {
|
|
|
57
64
|
* wrong data. Tying v3 rows to a message id for exact per-message attribution
|
|
58
65
|
* regardless of counter drift is a documented follow-up.
|
|
59
66
|
*
|
|
60
|
-
* Selection rows are stored in
|
|
61
|
-
*
|
|
62
|
-
* the block v3 would inject.
|
|
67
|
+
* Selection rows are stored in selection order, so rendering them in row order
|
|
68
|
+
* reproduces the block v3 would inject.
|
|
63
69
|
*/
|
|
64
70
|
export async function getMemoryV3SelectionForInspector(
|
|
65
71
|
conversationId: string,
|
|
@@ -106,13 +112,14 @@ export async function getMemoryV3SelectionForInspector(
|
|
|
106
112
|
* `memory_v3_selections` row for the conversation (all turns) and rolls them up
|
|
107
113
|
* for shadow-vs-v2 inspection without re-rendering any blocks:
|
|
108
114
|
*
|
|
109
|
-
* - `bySource`: count of selection rows per lane source (`
|
|
110
|
-
* `
|
|
115
|
+
* - `bySource`: count of selection rows per lane source (`core` / `hot` /
|
|
116
|
+
* `needle` / `dense` / `edge`). Every known source is present (zero when
|
|
111
117
|
* unused) so callers can diff two runs without null-guarding; an unknown
|
|
112
|
-
* historical/free-text source
|
|
118
|
+
* historical/free-text source — including retired labels like the old
|
|
119
|
+
* per-turn carry source — is ignored (the column is permissive).
|
|
113
120
|
* - `turns`: number of distinct turns that logged at least one selection.
|
|
114
121
|
* - `distinctSlugs`: number of distinct page slugs selected across all turns —
|
|
115
|
-
* the conversation's
|
|
122
|
+
* the conversation's selection footprint.
|
|
116
123
|
*
|
|
117
124
|
* This is read-only telemetry for comparing a shadow run's lane mix against
|
|
118
125
|
* v2's logged selections offline; it never re-runs orchestration.
|