@vellumai/assistant 0.8.0 → 0.8.2
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/AGENTS.md +11 -0
- package/ARCHITECTURE.md +2 -7
- package/Dockerfile +80 -5
- package/README.md +2 -2
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +21 -0
- package/docker-init-apt-root.sh +94 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
- package/eslint-rules/cli-no-daemon-internals.js +283 -0
- package/eslint.config.mjs +12 -0
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/knip.json +2 -1
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +4462 -991
- package/package.json +5 -1
- package/scripts/generate-openapi.ts +135 -14
- package/scripts/sync-llm-catalog.ts +165 -0
- package/scripts/sync-web-search-catalog.ts +129 -0
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +137 -2
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-control-flow.test.ts +7 -0
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/avatar-identity-sync.test.ts +87 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/call-site-routing-provider.test.ts +172 -45
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/channel-policy.test.ts +12 -0
- package/src/__tests__/checker.test.ts +89 -0
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +35 -7
- package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
- package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
- package/src/__tests__/config-loader-backfill.test.ts +526 -102
- package/src/__tests__/config-loader-corrupt.test.ts +68 -0
- package/src/__tests__/config-loader-platform-defaults.test.ts +77 -23
- package/src/__tests__/config-schema-cmd.test.ts +63 -29
- package/src/__tests__/config-schema.test.ts +35 -3
- package/src/__tests__/config-set-platform-guard.test.ts +75 -152
- package/src/__tests__/config-set-route.test.ts +278 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/config-watcher.test.ts +6 -0
- package/src/__tests__/contacts-tools.test.ts +51 -199
- package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
- package/src/__tests__/context-search-agent-runner.test.ts +22 -138
- package/src/__tests__/context-search-conversations-source.test.ts +159 -18
- package/src/__tests__/context-search-fanout.test.ts +20 -157
- package/src/__tests__/context-search-memory-v2-source.test.ts +3 -4
- package/src/__tests__/context-search-types.test.ts +7 -2
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +1 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +93 -92
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
- package/src/__tests__/conversation-error.test.ts +80 -3
- package/src/__tests__/conversation-fork-crud.test.ts +323 -1
- package/src/__tests__/conversation-inference-profile-route.test.ts +54 -18
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -0
- package/src/__tests__/conversation-lifecycle.test.ts +297 -0
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
- package/src/__tests__/conversation-process-callsite.test.ts +25 -2
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +80 -13
- package/src/__tests__/conversation-slash-commands.test.ts +194 -2
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -8
- package/src/__tests__/daemon-credential-client.test.ts +56 -1
- package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
- package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
- package/src/__tests__/db-proxy-transaction.test.ts +206 -0
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +482 -0
- package/src/__tests__/filing-service.test.ts +163 -3
- package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +42 -69
- package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
- package/src/__tests__/heartbeat-service.test.ts +50 -233
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +162 -0
- package/src/__tests__/host-app-control-proxy.test.ts +365 -1
- package/src/__tests__/host-app-control-routes.test.ts +247 -1
- package/src/__tests__/host-browser-proxy.test.ts +416 -20
- package/src/__tests__/host-browser-routes.test.ts +325 -33
- package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
- package/src/__tests__/inference-profile-reaper.test.ts +156 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +410 -0
- package/src/__tests__/inference-profile-session-ipc.test.ts +248 -0
- package/src/__tests__/injector-chain.test.ts +10 -8
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
- package/src/__tests__/install-skill-routing.test.ts +157 -39
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +107 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
- package/src/__tests__/llm-catalog-parity.test.ts +190 -2
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +222 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +46 -0
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +145 -14
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/mcp-auth-routes.test.ts +1 -0
- package/src/__tests__/mcp-cli.test.ts +182 -220
- package/src/__tests__/mcp-health-check.test.ts +56 -27
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/message-complete-display-id.test.ts +175 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/notification-platform-adapter.test.ts +229 -0
- package/src/__tests__/oauth-cli.test.ts +38 -2009
- package/src/__tests__/oauth-commands-routes.test.ts +863 -0
- package/src/__tests__/oauth-connect-routes.test.ts +174 -11
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/oauth-providers-routes.test.ts +14 -10
- package/src/__tests__/openai-provider.test.ts +24 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +48 -19
- package/src/__tests__/openai-responses-provider.test.ts +17 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +41 -38
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +31 -4
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -18
- package/src/__tests__/plugin-types.test.ts +15 -23
- package/src/__tests__/process-message-background-slack.test.ts +53 -0
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/profile-entry-status.test.ts +43 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +20 -12
- package/src/__tests__/provider-registry-ollama.test.ts +12 -4
- package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
- package/src/__tests__/relay-server.test.ts +118 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-retry.test.ts +56 -4
- package/src/__tests__/schedule-routes.test.ts +151 -0
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
- package/src/__tests__/scheduler-recurrence.test.ts +87 -34
- package/src/__tests__/scheduler-reuse-conversation.test.ts +208 -5
- package/src/__tests__/scheduler-wake.test.ts +0 -63
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/secret-allowlist.test.ts +1 -0
- package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +12 -4
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/shell-credential-ref.test.ts +95 -3
- package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -12
- package/src/__tests__/skill-load-tool.test.ts +29 -93
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/suggestion-routes.test.ts +3 -3
- package/src/__tests__/sync-message-contract.test.ts +63 -0
- package/src/__tests__/system-prompt.test.ts +737 -63
- package/src/__tests__/task-scheduler.test.ts +88 -23
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/update-bulletin-job.test.ts +96 -193
- package/src/__tests__/usage-cli.test.ts +11 -73
- package/src/__tests__/user-plugin-loader.test.ts +143 -5
- package/src/__tests__/vercel-config.test.ts +168 -0
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +108 -0
- package/src/__tests__/web-search.test.ts +303 -2
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +170 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +53 -20
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +241 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
- package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
- package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
- package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
- package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
- package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
- package/src/acp/__tests__/helpers/which-stub.ts +4 -2
- package/src/acp/resolve-agent.test.ts +25 -0
- package/src/acp/resolve-agent.ts +13 -2
- package/src/acp/session-manager.ts +14 -0
- package/src/agent/image-optimize.ts +13 -5
- package/src/approvals/guardian-request-resolvers.ts +32 -87
- package/src/calls/relay-server.ts +35 -0
- package/src/calls/relay-setup-router.ts +36 -0
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-session-bridge.ts +74 -36
- package/src/channels/config.ts +14 -1
- package/src/channels/types.ts +109 -0
- package/src/cli/AGENTS.md +164 -4
- package/src/cli/__tests__/notifications.test.ts +54 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/avatar.test.ts +540 -0
- package/src/cli/commands/__tests__/backup.test.ts +236 -776
- package/src/cli/commands/__tests__/cache.test.ts +1 -1
- package/src/cli/commands/__tests__/changelog.test.ts +578 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
- package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
- package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
- package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
- package/src/cli/commands/__tests__/email-core.test.ts +579 -0
- package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
- package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
- package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
- package/src/cli/commands/__tests__/schedules.test.ts +491 -0
- package/src/cli/commands/__tests__/skills.test.ts +563 -0
- package/src/cli/commands/__tests__/status.test.ts +249 -0
- package/src/cli/commands/__tests__/stt.test.ts +320 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
- package/src/cli/commands/__tests__/tts.test.ts +321 -0
- package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
- package/src/cli/commands/attachment.ts +8 -3
- package/src/cli/commands/audit.ts +95 -64
- package/src/cli/commands/auth.ts +61 -58
- package/src/cli/commands/avatar.ts +276 -390
- package/src/cli/commands/backup.ts +409 -505
- package/src/cli/commands/bash.ts +9 -5
- package/src/cli/commands/browser.ts +28 -9
- package/src/cli/commands/cache.ts +9 -4
- package/src/cli/commands/changelog.ts +478 -0
- package/src/cli/commands/channel-verification-sessions.ts +238 -317
- package/src/cli/commands/clients.ts +8 -3
- package/src/cli/commands/completions.ts +9 -9
- package/src/cli/commands/config.ts +102 -72
- package/src/cli/commands/contacts.ts +575 -696
- package/src/cli/commands/conversations-defer.ts +17 -69
- package/src/cli/commands/conversations-import.ts +90 -253
- package/src/cli/commands/conversations.ts +429 -434
- package/src/cli/commands/credential-execution.ts +9 -6
- package/src/cli/commands/credentials.ts +456 -736
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/domain.ts +128 -206
- package/src/cli/commands/email.ts +606 -794
- package/src/cli/commands/gateway.ts +8 -1
- package/src/cli/commands/image-generation.ts +157 -205
- package/src/cli/commands/inference-providers.ts +352 -0
- package/src/cli/commands/inference-session.ts +415 -0
- package/src/cli/commands/inference.ts +87 -65
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/mcp.ts +103 -287
- package/src/cli/commands/memory-v2.ts +162 -516
- package/src/cli/commands/notifications.ts +342 -304
- package/src/cli/commands/oauth/apps.ts +292 -261
- package/src/cli/commands/oauth/connect.ts +176 -297
- package/src/cli/commands/oauth/disconnect.ts +16 -215
- package/src/cli/commands/oauth/index.ts +49 -45
- package/src/cli/commands/oauth/mode.ts +43 -199
- package/src/cli/commands/oauth/ping.ts +17 -125
- package/src/cli/commands/oauth/providers.ts +732 -921
- package/src/cli/commands/oauth/request.ts +60 -350
- package/src/cli/commands/oauth/shared.ts +11 -121
- package/src/cli/commands/oauth/status.ts +31 -121
- package/src/cli/commands/oauth/token.ts +13 -55
- package/src/cli/commands/pending.ts +19 -10
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
- package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
- package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
- package/src/cli/commands/platform/connect.ts +16 -80
- package/src/cli/commands/platform/disconnect.ts +14 -112
- package/src/cli/commands/platform/index.ts +177 -246
- package/src/cli/commands/plugins.ts +185 -0
- package/src/cli/commands/routes.ts +153 -336
- package/src/cli/commands/schedules.ts +391 -0
- package/src/cli/commands/sequence.ts +316 -360
- package/src/cli/commands/skills.ts +449 -671
- package/src/cli/commands/status.ts +58 -37
- package/src/cli/commands/stt.ts +94 -262
- package/src/cli/commands/task.ts +14 -40
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/commands/trust.ts +8 -3
- package/src/cli/commands/tts.ts +162 -167
- package/src/cli/commands/ui.ts +35 -42
- package/src/cli/commands/usage.ts +188 -126
- package/src/cli/commands/watchers.ts +8 -3
- package/src/cli/commands/webhooks.ts +99 -193
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/register-command.test.ts +85 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/daemon-credential-client.ts +4 -5
- package/src/cli/lib/install-from-github.ts +304 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/nested-value.ts +44 -0
- package/src/cli/lib/open-browser.ts +36 -0
- package/src/cli/lib/register-command.ts +19 -0
- package/src/cli/lib/time-ago.ts +34 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +40 -6
- package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
- package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
- package/src/cli/utils/conversation-id.ts +30 -0
- package/src/cli/utils/parse-duration.ts +41 -0
- package/src/config/acp-defaults.test.ts +5 -1
- package/src/config/acp-defaults.ts +11 -4
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/contacts/SKILL.md +12 -45
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -2
- package/src/config/feature-flag-registry.json +57 -1
- package/src/config/llm-resolver.ts +16 -1
- package/src/config/loader.ts +140 -52
- package/src/config/raw-config-utils.ts +2 -30
- package/src/config/schema.ts +8 -7
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
- package/src/config/schemas/call-site-catalog.ts +29 -7
- package/src/config/schemas/channels.ts +8 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm-request-logs.ts +81 -0
- package/src/config/schemas/llm.ts +55 -2
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/memory-retrospective.ts +48 -0
- package/src/config/schemas/memory-v2.ts +32 -1
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/services.ts +15 -12
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/seed-inference-profiles.ts +195 -134
- package/src/config/skills.ts +3 -96
- package/src/contacts/contact-store.ts +0 -61
- package/src/context/compactor.ts +1047 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/context/window-manager.ts +197 -1334
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +113 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +183 -4
- package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
- package/src/daemon/approval-generators.ts +26 -30
- package/src/daemon/config-watcher.ts +94 -29
- package/src/daemon/conversation-agent-loop-handlers.ts +24 -0
- package/src/daemon/conversation-agent-loop.ts +293 -103
- package/src/daemon/conversation-error.ts +188 -33
- package/src/daemon/conversation-lifecycle.ts +80 -26
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +85 -31
- package/src/daemon/conversation-runtime-assembly.ts +30 -6
- package/src/daemon/conversation-slash.ts +184 -25
- package/src/daemon/conversation-store.ts +24 -10
- package/src/daemon/conversation-surfaces.ts +76 -12
- package/src/daemon/conversation-tool-setup.ts +63 -21
- package/src/daemon/conversation.ts +81 -10
- package/src/daemon/external-plugins-bootstrap.ts +231 -185
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/guardian-action-generators.ts +7 -22
- package/src/daemon/handlers/config-model.ts +13 -130
- package/src/daemon/handlers/config-slack-channel.ts +25 -10
- package/src/daemon/handlers/config-vercel.ts +3 -1
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +166 -84
- package/src/daemon/history-repair.ts +61 -7
- package/src/daemon/host-app-control-proxy.ts +129 -29
- package/src/daemon/host-bash-proxy.ts +85 -158
- package/src/daemon/host-browser-proxy.ts +96 -35
- package/src/daemon/host-proxy-base.ts +13 -1
- package/src/daemon/host-proxy-preactivation.ts +25 -1
- package/src/daemon/identity-helpers.ts +19 -0
- package/src/daemon/lifecycle.ts +79 -70
- package/src/daemon/meet-host-supervisor.ts +20 -19
- package/src/daemon/memory-v2-startup.ts +58 -2
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/bookmarks.ts +18 -0
- package/src/daemon/message-types/conversations.ts +37 -9
- package/src/daemon/message-types/messages.ts +70 -1
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +61 -0
- package/src/daemon/pkb-reminder-builder.test.ts +54 -13
- package/src/daemon/pkb-reminder-builder.ts +21 -7
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +77 -26
- package/src/daemon/server.ts +34 -20
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/skill-memory-refresh.ts +29 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/daemon/tool-side-effects.ts +6 -4
- package/src/daemon/wake-target-adapter.ts +11 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/export/transcript-formatter.ts +61 -2
- package/src/filing/filing-service.ts +79 -53
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +444 -0
- package/src/heartbeat/heartbeat-run-store.ts +3 -1
- package/src/heartbeat/heartbeat-service.ts +189 -127
- package/src/home/__tests__/feed-types.test.ts +99 -127
- package/src/home/__tests__/feed-writer.test.ts +77 -278
- package/src/home/__tests__/post-connect-feed.test.ts +9 -12
- package/src/home/feed-types.ts +41 -73
- package/src/home/feed-writer.ts +25 -156
- package/src/home/post-connect-feed.ts +2 -3
- package/src/index.ts +18 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
- package/src/ipc/__tests__/email-ipc.test.ts +506 -0
- package/src/ipc/__tests__/exit-helper.test.ts +104 -0
- package/src/ipc/__tests__/streaming-client.test.ts +237 -0
- package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
- package/src/ipc/assistant-server.ts +55 -6
- package/src/ipc/cli-client.ts +370 -50
- package/src/ipc/routes/db-proxy-transaction.ts +151 -0
- package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
- package/src/ipc/skill-routes/events.ts +30 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
- package/src/live-voice/live-voice-session-manager.ts +11 -4
- package/src/live-voice/live-voice-session.ts +14 -6
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +264 -0
- package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
- package/src/memory/__tests__/conversation-queries.test.ts +263 -0
- package/src/memory/__tests__/conversation-types.test.ts +36 -0
- package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +318 -0
- package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +211 -0
- package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
- package/src/memory/context-search/agent-protocol.ts +5 -1
- package/src/memory/context-search/agent-runner.ts +60 -85
- package/src/memory/context-search/limits.ts +1 -4
- package/src/memory/context-search/search.ts +23 -113
- package/src/memory/context-search/sources/conversations.ts +80 -8
- package/src/memory/context-search/sources/memory-v2.ts +39 -14
- package/src/memory/context-search/sources/memory.ts +7 -0
- package/src/memory/context-search/sources/workspace.ts +17 -10
- package/src/memory/context-search/types.ts +1 -1
- package/src/memory/conversation-bootstrap.ts +11 -0
- package/src/memory/conversation-crud.ts +368 -22
- package/src/memory/conversation-queries.ts +116 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/conversation-types.ts +16 -0
- package/src/memory/db-init.ts +20 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +6 -5
- package/src/memory/embedding-runtime-manager.ts +1 -2
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
- package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
- package/src/memory/graph/conversation-graph-memory.ts +92 -5
- package/src/memory/graph/extraction.ts +4 -0
- package/src/memory/graph/graph-memory-state-store.ts +16 -3
- package/src/memory/graph/tool-handlers.ts +17 -7
- package/src/memory/graph/tools.ts +45 -6
- package/src/memory/indexer.ts +51 -29
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +86 -15
- package/src/memory/jobs/embed-concept-page.ts +65 -20
- package/src/memory/jobs-store.ts +51 -1
- package/src/memory/jobs-worker.ts +57 -3
- package/src/memory/llm-request-log-source-clickhouse.ts +324 -0
- package/src/memory/llm-request-log-source-local.ts +26 -0
- package/src/memory/llm-request-log-source.ts +64 -0
- package/src/memory/llm-request-log-store.ts +1 -1
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-constants.ts +13 -0
- package/src/memory/memory-retrospective-enqueue.ts +114 -0
- package/src/memory/memory-retrospective-job.ts +351 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +175 -0
- package/src/memory/memory-retrospective-state.ts +162 -0
- package/src/memory/memory-retrospective-trigger-check.ts +91 -0
- package/src/memory/memory-v2-activation-log-store.ts +49 -5
- package/src/memory/memory-v2-concept-frequency.ts +4 -0
- package/src/memory/message-content.ts +38 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
- package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
- package/src/memory/migrations/229-delete-private-conversations.test.ts +107 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +19 -0
- package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
- package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
- package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
- package/src/memory/migrations/242-message-bookmarks.ts +38 -0
- package/src/memory/migrations/243-provider-connections.ts +68 -0
- package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
- package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
- package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
- package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
- package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
- package/src/memory/migrations/index.ts +13 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/published-pages-store.ts +16 -0
- package/src/memory/schema/bookmarks.ts +36 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/inference.ts +27 -0
- package/src/memory/schema/infrastructure.ts +12 -0
- package/src/memory/schema/memory-core.ts +9 -0
- package/src/memory/search/semantic.ts +1 -4
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
- package/src/memory/v2/__tests__/activation.test.ts +11 -12
- package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
- package/src/memory/v2/__tests__/consolidation-job.test.ts +123 -135
- package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
- package/src/memory/v2/__tests__/injection.test.ts +726 -18
- package/src/memory/v2/__tests__/migration.test.ts +94 -3
- package/src/memory/v2/__tests__/page-index.test.ts +360 -0
- package/src/memory/v2/__tests__/page-store.test.ts +14 -1
- package/src/memory/v2/__tests__/prompts-router.test.ts +309 -0
- package/src/memory/v2/__tests__/qdrant.test.ts +138 -3
- package/src/memory/v2/__tests__/reranker.test.ts +4 -4
- package/src/memory/v2/__tests__/router.test.ts +531 -0
- package/src/memory/v2/__tests__/sim.test.ts +45 -1
- package/src/memory/v2/__tests__/skill-store.test.ts +445 -11
- package/src/memory/v2/__tests__/static-context.test.ts +7 -22
- package/src/memory/v2/__tests__/sweep-job.test.ts +95 -0
- package/src/memory/v2/activation-store.ts +34 -5
- package/src/memory/v2/activation.ts +40 -27
- package/src/memory/v2/backfill-jobs.ts +17 -84
- package/src/memory/v2/consolidation-job.ts +85 -78
- package/src/memory/v2/frontmatter-sweep.ts +91 -0
- package/src/memory/v2/injection.ts +466 -109
- package/src/memory/v2/migration.ts +147 -20
- package/src/memory/v2/page-index.ts +221 -0
- package/src/memory/v2/page-store.ts +3 -0
- package/src/memory/v2/prompts/consolidation.ts +9 -7
- package/src/memory/v2/prompts/router.ts +195 -0
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +234 -93
- package/src/memory/v2/reranker.ts +14 -7
- package/src/memory/v2/router.ts +323 -0
- package/src/memory/v2/sim.ts +25 -12
- package/src/memory/v2/skill-store.ts +204 -30
- package/src/memory/v2/static-context.ts +16 -9
- package/src/memory/v2/sweep-job.ts +122 -96
- package/src/memory/v2/types.ts +10 -6
- package/src/memory/validation.ts +13 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
- package/src/notifications/__tests__/signal-registry.test.ts +17 -0
- package/src/notifications/adapters/platform.ts +171 -0
- package/src/notifications/conversation-pairing.ts +4 -3
- package/src/notifications/copy-composer.ts +15 -0
- package/src/notifications/decision-engine.ts +2 -1
- package/src/notifications/destination-resolver.ts +21 -0
- package/src/notifications/emit-signal.ts +48 -2
- package/src/notifications/home-feed-side-effect.ts +165 -0
- package/src/notifications/signal.ts +8 -1
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.ts +6 -2
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +14 -0
- package/src/permissions/ipc-risk-types.ts +3 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +46 -0
- package/src/plugin-api/package.json +12 -0
- package/src/plugin-api/types.ts +144 -0
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +55 -6
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +367 -0
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +74 -53
- package/src/plugins/user-loader.ts +85 -43
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +49 -9
- package/src/proactive-artifact/job.ts +4 -0
- package/src/proactive-artifact/trigger-state.test.ts +9 -0
- package/src/proactive-artifact/trigger-state.ts +4 -0
- package/src/prompts/__tests__/system-prompt.test.ts +117 -0
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +72 -154
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +173 -0
- package/src/prompts/update-bulletin-job.ts +61 -73
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
- package/src/providers/__tests__/inference.test.ts +303 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
- package/src/providers/__tests__/retry-callsite.test.ts +14 -32
- package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
- package/src/providers/anthropic/client.ts +123 -54
- package/src/providers/call-site-routing.ts +94 -16
- package/src/providers/connection-resolution.ts +170 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
- package/src/providers/inference/adapter-factory.ts +210 -0
- package/src/providers/inference/auth.ts +112 -0
- package/src/providers/inference/backfill.ts +196 -0
- package/src/providers/inference/connections.ts +401 -0
- package/src/providers/inference/resolve-auth.ts +73 -0
- package/src/providers/model-catalog.ts +386 -6
- package/src/providers/openai/chat-completions-provider.ts +10 -2
- package/src/providers/openai/responses-provider.ts +4 -2
- package/src/providers/openrouter/client.ts +7 -0
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +36 -0
- package/src/providers/provider-env-vars.ts +17 -7
- package/src/providers/provider-secret-catalog.ts +49 -30
- package/src/providers/provider-send-message.ts +41 -20
- package/src/providers/registry.ts +151 -159
- package/src/providers/retry.ts +65 -11
- package/src/providers/search-provider-catalog.ts +121 -0
- package/src/runtime/AGENTS.md +18 -5
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
- package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
- package/src/runtime/actor-trust-resolver.ts +32 -10
- package/src/runtime/agent-wake.ts +64 -7
- package/src/runtime/assistant-event-hub.ts +3 -85
- package/src/runtime/auth/route-policy.ts +311 -9
- package/src/runtime/auth/same-actor.ts +2 -0
- package/src/runtime/background-job-runner.ts +339 -0
- package/src/runtime/btw-sidechain.ts +3 -0
- package/src/runtime/http-router.ts +36 -1
- package/src/runtime/http-server.ts +31 -5
- package/src/runtime/http-types.ts +21 -0
- package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
- package/src/runtime/middleware/request-logger.ts +62 -1
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/pre-first-message-gate.ts +83 -0
- package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +268 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +319 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +280 -4
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +4 -4
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +70 -3
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +12 -8
- package/src/runtime/routes/app-management-routes.ts +228 -3
- package/src/runtime/routes/approval-routes.ts +0 -18
- package/src/runtime/routes/audit-routes.ts +43 -0
- package/src/runtime/routes/auth-routes.ts +72 -0
- package/src/runtime/routes/avatar-routes.ts +273 -20
- package/src/runtime/routes/backup-routes.ts +406 -2
- package/src/runtime/routes/bookmark-routes.ts +156 -0
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +121 -0
- package/src/runtime/routes/channel-verification-routes.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +0 -160
- package/src/runtime/routes/conversation-cli-routes.ts +233 -0
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +47 -85
- package/src/runtime/routes/conversation-query-routes.ts +350 -97
- package/src/runtime/routes/conversation-routes.ts +121 -21
- package/src/runtime/routes/conversations-import-routes.ts +229 -0
- package/src/runtime/routes/credential-routes.ts +540 -0
- package/src/runtime/routes/debug-routes.ts +2 -2
- package/src/runtime/routes/document-pdf-renderer.ts +5 -1
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/domain-routes.ts +167 -0
- package/src/runtime/routes/email-routes.ts +603 -0
- package/src/runtime/routes/errors.ts +2 -2
- package/src/runtime/routes/events-routes.ts +192 -0
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -78
- package/src/runtime/routes/host-app-control-routes.ts +44 -2
- package/src/runtime/routes/host-browser-routes.ts +103 -22
- package/src/runtime/routes/http-adapter.ts +2 -0
- package/src/runtime/routes/identity-routes.ts +5 -0
- package/src/runtime/routes/image-generation-routes.ts +99 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +248 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +118 -7
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -4
- package/src/runtime/routes/index.ts +42 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +285 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +84 -0
- package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +361 -0
- package/src/runtime/routes/inference-send-routes.ts +115 -0
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +7 -13
- package/src/runtime/routes/mcp-auth-routes.ts +283 -9
- package/src/runtime/routes/memory-v2-routes.ts +13 -398
- package/src/runtime/routes/notification-routes.ts +3 -1
- package/src/runtime/routes/oauth-apps.ts +112 -7
- package/src/runtime/routes/oauth-commands-routes.ts +1097 -0
- package/src/runtime/routes/oauth-connect-routes.ts +67 -5
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/oauth-providers.ts +298 -8
- package/src/runtime/routes/platform-routes.ts +336 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -1
- package/src/runtime/routes/playground/reset-circuit.ts +2 -1
- package/src/runtime/routes/playground/state.ts +2 -1
- package/src/runtime/routes/publish-routes.ts +221 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +79 -0
- package/src/runtime/routes/sequence-routes.ts +291 -0
- package/src/runtime/routes/settings-routes.ts +2 -10
- package/src/runtime/routes/skills-routes.ts +31 -1
- package/src/runtime/routes/stt-routes.ts +240 -3
- package/src/runtime/routes/subagents-routes.ts +57 -18
- package/src/runtime/routes/surface-action-routes.ts +43 -7
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +93 -1
- package/src/runtime/routes/types.ts +32 -0
- package/src/runtime/routes/user-routes-cli.ts +243 -0
- package/src/runtime/routes/webhook-routes.ts +165 -0
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +117 -0
- package/src/runtime/sync/sync-publisher.test.ts +105 -0
- package/src/runtime/sync/sync-publisher.ts +21 -0
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +208 -123
- package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/sequence/engine.ts +38 -40
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +28 -15
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
- package/src/tools/browser/browser-execution.ts +29 -14
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
- package/src/tools/browser/cdp-client/factory.ts +66 -5
- package/src/tools/browser/runtime-check.ts +77 -0
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/memory/register.test.ts +3 -3
- package/src/tools/memory/register.ts +9 -1
- package/src/tools/network/__tests__/web-search.test.ts +156 -0
- package/src/tools/network/web-search.ts +280 -37
- package/src/tools/permission-checker.ts +14 -6
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/subagent/spawn.ts +3 -3
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/terminal/shell.ts +44 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +72 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/usage/attribution.ts +3 -2
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/util/pricing.ts +86 -160
- package/src/watcher/__tests__/engine.test.ts +323 -0
- package/src/watcher/constants.ts +7 -0
- package/src/watcher/engine.ts +94 -90
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
- package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +94 -5
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +8 -2
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +117 -0
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +95 -0
- package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
- package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
- package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
- package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
- package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
- package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
- package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
- package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
- package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/registry.ts +30 -0
- package/src/workspace/migrations/runner.ts +46 -5
- package/src/workspace/migrations/types.ts +17 -3
- package/src/workspace/provider-commit-message-generator.ts +3 -2
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-search-pkb-source.test.ts +0 -498
- package/src/__tests__/context-window-manager.test.ts +0 -2093
- package/src/__tests__/credentials-cli.test.ts +0 -1225
- package/src/__tests__/memory-admin-recall.test.ts +0 -213
- package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
- package/src/cli/commands/__tests__/email-download.test.ts +0 -260
- package/src/cli/commands/__tests__/email-list.test.ts +0 -216
- package/src/cli/commands/__tests__/email-register.test.ts +0 -186
- package/src/cli/commands/__tests__/email-send.test.ts +0 -416
- package/src/cli/commands/__tests__/email-status.test.ts +0 -185
- package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
- package/src/cli/commands/__tests__/routes.test.ts +0 -562
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
- package/src/cli/commands/autonomy.ts +0 -365
- package/src/cli/commands/memory.ts +0 -424
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -947
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
- package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
- package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
- package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
- package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
- package/src/cli/lib/daemon-avatar-client.ts +0 -37
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
- package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
- package/src/home/__tests__/emit-feed-event.test.ts +0 -169
- package/src/home/__tests__/feed-population-integration.test.ts +0 -312
- package/src/home/__tests__/feed-scheduler.test.ts +0 -222
- package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
- package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
- package/src/home/__tests__/rollup-producer.test.ts +0 -507
- package/src/home/assistant-feed-authoring.ts +0 -135
- package/src/home/emit-feed-event.ts +0 -169
- package/src/home/feed-scheduler.ts +0 -281
- package/src/home/platform-gmail-digest.ts +0 -163
- package/src/home/rewrite-command-preview.ts +0 -66
- package/src/home/rewrite-feed-title.ts +0 -58
- package/src/home/rollup-producer.ts +0 -426
- package/src/memory/admin.ts +0 -326
- package/src/memory/context-search/sources/pkb.ts +0 -476
- package/src/memory/graph/compaction.ts +0 -299
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- /package/src/cli/{commands → lib}/cache-fs.ts +0 -0
|
@@ -1,2093 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { ContextWindowConfig } from "../config/types.js";
|
|
4
|
-
import { estimateTextTokens } from "../context/token-estimator.js";
|
|
5
|
-
import {
|
|
6
|
-
clampSummaryAtSectionBoundary,
|
|
7
|
-
CONTEXT_SUMMARY_MARKER,
|
|
8
|
-
ContextWindowManager,
|
|
9
|
-
createContextSummaryMessage,
|
|
10
|
-
getSummaryFromContextMessage,
|
|
11
|
-
stripCompactionOnlyInjections,
|
|
12
|
-
} from "../context/window-manager.js";
|
|
13
|
-
import type {
|
|
14
|
-
ContentBlock,
|
|
15
|
-
Message,
|
|
16
|
-
Provider,
|
|
17
|
-
ProviderResponse,
|
|
18
|
-
SendMessageOptions,
|
|
19
|
-
} from "../providers/types.js";
|
|
20
|
-
|
|
21
|
-
function makeConfig(
|
|
22
|
-
overrides: Partial<ContextWindowConfig> = {},
|
|
23
|
-
): ContextWindowConfig {
|
|
24
|
-
return {
|
|
25
|
-
enabled: true,
|
|
26
|
-
maxInputTokens: 450,
|
|
27
|
-
targetBudgetRatio: 0.67,
|
|
28
|
-
compactThreshold: 0.6,
|
|
29
|
-
summaryBudgetRatio: 0.05,
|
|
30
|
-
overflowRecovery: {
|
|
31
|
-
enabled: true,
|
|
32
|
-
safetyMarginRatio: 0.05,
|
|
33
|
-
maxAttempts: 3,
|
|
34
|
-
interactiveLatestTurnCompression: "summarize",
|
|
35
|
-
nonInteractiveLatestTurnCompression: "truncate",
|
|
36
|
-
},
|
|
37
|
-
...overrides,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createProvider(
|
|
42
|
-
fn: (messages: Message[]) => ProviderResponse | Promise<ProviderResponse>,
|
|
43
|
-
name: string = "mock",
|
|
44
|
-
): Provider {
|
|
45
|
-
return {
|
|
46
|
-
name,
|
|
47
|
-
async sendMessage(messages: Message[]): Promise<ProviderResponse> {
|
|
48
|
-
return fn(messages);
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function message(role: "user" | "assistant", text: string): Message {
|
|
54
|
-
return { role, content: [{ type: "text", text }] };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe("ContextWindowManager", () => {
|
|
58
|
-
test("skips compaction when estimated tokens are below threshold", async () => {
|
|
59
|
-
const provider = createProvider(() => {
|
|
60
|
-
throw new Error("should not be called");
|
|
61
|
-
});
|
|
62
|
-
const manager = new ContextWindowManager({
|
|
63
|
-
provider,
|
|
64
|
-
systemPrompt: "system prompt",
|
|
65
|
-
config: makeConfig(),
|
|
66
|
-
});
|
|
67
|
-
const history = [message("user", "hello"), message("assistant", "hi")];
|
|
68
|
-
|
|
69
|
-
const result = await manager.maybeCompact(history);
|
|
70
|
-
expect(result.compacted).toBe(false);
|
|
71
|
-
expect(result.messages).toEqual(history);
|
|
72
|
-
expect(result.reason).toBe("below compaction threshold");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("explains forced compaction skip when conversation already fits target", async () => {
|
|
76
|
-
const provider = createProvider(() => {
|
|
77
|
-
throw new Error("summarizer should not be called");
|
|
78
|
-
});
|
|
79
|
-
const manager = new ContextWindowManager({
|
|
80
|
-
provider,
|
|
81
|
-
systemPrompt: "system prompt",
|
|
82
|
-
config: makeConfig({
|
|
83
|
-
maxInputTokens: 10_000,
|
|
84
|
-
targetBudgetRatio: 0.5,
|
|
85
|
-
}),
|
|
86
|
-
});
|
|
87
|
-
const history = [message("user", "hello"), message("assistant", "hi")];
|
|
88
|
-
|
|
89
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
90
|
-
force: true,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(result.compacted).toBe(false);
|
|
94
|
-
expect(result.messages).toEqual(history);
|
|
95
|
-
expect(result.reason).toBe(
|
|
96
|
-
"conversation already fits within the compaction target",
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("forced compaction summarizes when projection fits but real usage exceeds target", async () => {
|
|
101
|
-
let summaryCalls = 0;
|
|
102
|
-
const provider = createProvider(() => {
|
|
103
|
-
summaryCalls += 1;
|
|
104
|
-
return {
|
|
105
|
-
content: [
|
|
106
|
-
{ type: "text", text: "## Summary\n- forced compaction ran" },
|
|
107
|
-
],
|
|
108
|
-
model: "mock-model",
|
|
109
|
-
usage: { inputTokens: 100, outputTokens: 25 },
|
|
110
|
-
stopReason: "end_turn",
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
const manager = new ContextWindowManager({
|
|
114
|
-
provider,
|
|
115
|
-
systemPrompt: "system prompt",
|
|
116
|
-
config: makeConfig({
|
|
117
|
-
maxInputTokens: 10_000,
|
|
118
|
-
targetBudgetRatio: 0.5,
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
121
|
-
// Tiny live messages so the projection trivially fits target — without
|
|
122
|
-
// the fix this would route through the "already fits" skip path.
|
|
123
|
-
const history: Message[] = [
|
|
124
|
-
message("user", "u1"),
|
|
125
|
-
message("assistant", "a1"),
|
|
126
|
-
message("user", "u2"),
|
|
127
|
-
message("assistant", "a2"),
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
131
|
-
force: true,
|
|
132
|
-
// Simulate a live conversation that's well over target. In production
|
|
133
|
-
// this happens when synthetic tool_result truncation in the projection
|
|
134
|
-
// is far more aggressive than what the real messages allow.
|
|
135
|
-
precomputedEstimate: 50_000,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(result.compacted).toBe(true);
|
|
139
|
-
expect(summaryCalls).toBe(1);
|
|
140
|
-
expect(result.reason).not.toBe(
|
|
141
|
-
"conversation already fits within the compaction target",
|
|
142
|
-
);
|
|
143
|
-
expect(result.compactedMessages).toBeGreaterThan(0);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("compacts old turns and keeps recent user turns", async () => {
|
|
147
|
-
let summaryCalls = 0;
|
|
148
|
-
const provider = createProvider(() => {
|
|
149
|
-
summaryCalls += 1;
|
|
150
|
-
return {
|
|
151
|
-
content: [
|
|
152
|
-
{ type: "text", text: `## Goals\n- summary call ${summaryCalls}` },
|
|
153
|
-
],
|
|
154
|
-
model: "mock-model",
|
|
155
|
-
usage: { inputTokens: 100, outputTokens: 25 },
|
|
156
|
-
stopReason: "end_turn",
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
const manager = new ContextWindowManager({
|
|
160
|
-
provider,
|
|
161
|
-
systemPrompt: "system prompt",
|
|
162
|
-
config: makeConfig({ maxInputTokens: 600 }),
|
|
163
|
-
});
|
|
164
|
-
const long = "x".repeat(240);
|
|
165
|
-
const history: Message[] = [
|
|
166
|
-
message("user", `u1 ${long}`),
|
|
167
|
-
message("assistant", `a1 ${long}`),
|
|
168
|
-
message("user", `u2 ${long}`),
|
|
169
|
-
message("assistant", `a2 ${long}`),
|
|
170
|
-
message("user", `u3 ${long}`),
|
|
171
|
-
message("assistant", `a3 ${long}`),
|
|
172
|
-
];
|
|
173
|
-
|
|
174
|
-
const result = await manager.maybeCompact(history);
|
|
175
|
-
|
|
176
|
-
expect(result.compacted).toBe(true);
|
|
177
|
-
expect(result.compactedMessages).toBeGreaterThan(0);
|
|
178
|
-
expect(result.summaryCalls).toBe(summaryCalls);
|
|
179
|
-
expect(result.summaryInputTokens).toBeGreaterThan(0);
|
|
180
|
-
expect(result.summaryOutputTokens).toBeGreaterThan(0);
|
|
181
|
-
expect(result.messages[0].role).toBe("user");
|
|
182
|
-
expect(
|
|
183
|
-
getSummaryFromContextMessage(result.messages[0])?.length,
|
|
184
|
-
).toBeGreaterThan(0);
|
|
185
|
-
|
|
186
|
-
const userTexts = result.messages
|
|
187
|
-
.filter((m) => m.role === "user")
|
|
188
|
-
.map((m) => (m.content[0].type === "text" ? m.content[0].text : ""));
|
|
189
|
-
expect(userTexts.some((text) => text.startsWith("u1 "))).toBe(false);
|
|
190
|
-
expect(userTexts.some((text) => text.startsWith("u2 "))).toBe(true);
|
|
191
|
-
expect(userTexts.some((text) => text.startsWith("u3 "))).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("returns cache-aware summary usage from single-pass compaction", async () => {
|
|
195
|
-
const provider = createProvider(() => {
|
|
196
|
-
return {
|
|
197
|
-
content: [
|
|
198
|
-
{ type: "text", text: `## Goals\n- summary of full transcript` },
|
|
199
|
-
],
|
|
200
|
-
model: "claude-opus-4-6",
|
|
201
|
-
usage: {
|
|
202
|
-
inputTokens: 5_000,
|
|
203
|
-
outputTokens: 80,
|
|
204
|
-
cacheCreationInputTokens: 50,
|
|
205
|
-
cacheReadInputTokens: 200,
|
|
206
|
-
},
|
|
207
|
-
rawResponse: {
|
|
208
|
-
usage: {
|
|
209
|
-
cache_creation: {
|
|
210
|
-
ephemeral_5m_input_tokens: 50,
|
|
211
|
-
ephemeral_1h_input_tokens: 0,
|
|
212
|
-
},
|
|
213
|
-
cache_read_input_tokens: 200,
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
stopReason: "end_turn",
|
|
217
|
-
};
|
|
218
|
-
});
|
|
219
|
-
const manager = new ContextWindowManager({
|
|
220
|
-
provider,
|
|
221
|
-
systemPrompt: "system prompt",
|
|
222
|
-
config: makeConfig({
|
|
223
|
-
maxInputTokens: 7_000,
|
|
224
|
-
targetBudgetRatio: 0.41,
|
|
225
|
-
}),
|
|
226
|
-
});
|
|
227
|
-
const long = "q".repeat(6_000);
|
|
228
|
-
const history: Message[] = [
|
|
229
|
-
message("user", `u1 ${long}`),
|
|
230
|
-
message("assistant", `a1 ${long}`),
|
|
231
|
-
message("user", `u2 ${long}`),
|
|
232
|
-
message("assistant", `a2 ${long}`),
|
|
233
|
-
message("user", `u3 ${long}`),
|
|
234
|
-
];
|
|
235
|
-
|
|
236
|
-
const result = await manager.maybeCompact(history);
|
|
237
|
-
|
|
238
|
-
expect(result.compacted).toBe(true);
|
|
239
|
-
expect(result.summaryCalls).toBe(1);
|
|
240
|
-
expect(result.summaryCacheCreationInputTokens).toBe(50);
|
|
241
|
-
expect(result.summaryCacheReadInputTokens).toBe(200);
|
|
242
|
-
expect(result.summaryRawResponses).toHaveLength(1);
|
|
243
|
-
expect(result.summaryRawResponses?.[0]).toMatchObject({
|
|
244
|
-
usage: {
|
|
245
|
-
cache_creation: { ephemeral_5m_input_tokens: 50 },
|
|
246
|
-
cache_read_input_tokens: 200,
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("updates an existing summary message instead of nesting summaries", async () => {
|
|
252
|
-
const provider = createProvider(() => ({
|
|
253
|
-
content: [{ type: "text", text: "## Goals\n- updated summary" }],
|
|
254
|
-
model: "mock-model",
|
|
255
|
-
usage: { inputTokens: 50, outputTokens: 10 },
|
|
256
|
-
stopReason: "end_turn",
|
|
257
|
-
}));
|
|
258
|
-
const manager = new ContextWindowManager({
|
|
259
|
-
provider,
|
|
260
|
-
systemPrompt: "system prompt",
|
|
261
|
-
config: makeConfig({
|
|
262
|
-
maxInputTokens: 300,
|
|
263
|
-
targetBudgetRatio: 0.58,
|
|
264
|
-
}),
|
|
265
|
-
});
|
|
266
|
-
const long = "y".repeat(220);
|
|
267
|
-
const history: Message[] = [
|
|
268
|
-
createContextSummaryMessage("## Goals\n- old summary"),
|
|
269
|
-
message("user", `older ${long}`),
|
|
270
|
-
message("assistant", `reply ${long}`),
|
|
271
|
-
message("user", `latest ${long}`),
|
|
272
|
-
];
|
|
273
|
-
|
|
274
|
-
const result = await manager.maybeCompact(history);
|
|
275
|
-
expect(result.compacted).toBe(true);
|
|
276
|
-
expect(result.messages.length).toBeLessThan(history.length + 1);
|
|
277
|
-
expect(getSummaryFromContextMessage(result.messages[0])).toContain(
|
|
278
|
-
"updated summary",
|
|
279
|
-
);
|
|
280
|
-
expect(
|
|
281
|
-
result.messages.filter(
|
|
282
|
-
(m) =>
|
|
283
|
-
m.role === "user" &&
|
|
284
|
-
m.content.some(
|
|
285
|
-
(block) =>
|
|
286
|
-
block.type === "text" &&
|
|
287
|
-
block.text.startsWith(CONTEXT_SUMMARY_MARKER),
|
|
288
|
-
),
|
|
289
|
-
),
|
|
290
|
-
).toHaveLength(1);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test("falls back to local summary when provider summarization fails", async () => {
|
|
294
|
-
const provider = createProvider(async () => {
|
|
295
|
-
throw new Error("provider unavailable");
|
|
296
|
-
});
|
|
297
|
-
const manager = new ContextWindowManager({
|
|
298
|
-
provider,
|
|
299
|
-
systemPrompt: "system prompt",
|
|
300
|
-
config: makeConfig({
|
|
301
|
-
maxInputTokens: 260,
|
|
302
|
-
targetBudgetRatio: 0.59,
|
|
303
|
-
}),
|
|
304
|
-
});
|
|
305
|
-
const long = "z".repeat(220);
|
|
306
|
-
const history = [
|
|
307
|
-
message("user", `task ${long}`),
|
|
308
|
-
message("assistant", `result ${long}`),
|
|
309
|
-
message("user", `followup ${long}`),
|
|
310
|
-
];
|
|
311
|
-
|
|
312
|
-
const result = await manager.maybeCompact(history);
|
|
313
|
-
expect(result.compacted).toBe(true);
|
|
314
|
-
expect(result.summaryCalls).toBeGreaterThan(0);
|
|
315
|
-
expect(result.summaryInputTokens).toBe(0);
|
|
316
|
-
expect(result.summaryOutputTokens).toBe(0);
|
|
317
|
-
expect(result.summaryModel).toBe("");
|
|
318
|
-
expect(result.summaryText).toContain("## Recent Progress");
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("marks summaryFailed when the provider throws and fallback runs", async () => {
|
|
322
|
-
// The agent-loop circuit breaker distinguishes "LLM call failed but
|
|
323
|
-
// fallback rescued us" from "compaction succeeded end-to-end". The
|
|
324
|
-
// fallback path must set summaryFailed:true so callers can count
|
|
325
|
-
// consecutive failures without losing the compacted messages.
|
|
326
|
-
const provider = createProvider(async () => {
|
|
327
|
-
throw new Error("provider unavailable");
|
|
328
|
-
});
|
|
329
|
-
const manager = new ContextWindowManager({
|
|
330
|
-
provider,
|
|
331
|
-
systemPrompt: "system prompt",
|
|
332
|
-
config: makeConfig({
|
|
333
|
-
maxInputTokens: 260,
|
|
334
|
-
targetBudgetRatio: 0.59,
|
|
335
|
-
}),
|
|
336
|
-
});
|
|
337
|
-
const long = "z".repeat(220);
|
|
338
|
-
const history = [
|
|
339
|
-
message("user", `task ${long}`),
|
|
340
|
-
message("assistant", `result ${long}`),
|
|
341
|
-
message("user", `followup ${long}`),
|
|
342
|
-
];
|
|
343
|
-
|
|
344
|
-
const result = await manager.maybeCompact(history);
|
|
345
|
-
expect(result.compacted).toBe(true);
|
|
346
|
-
expect(result.summaryFailed).toBe(true);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test("does not mark summaryFailed on a successful provider call", async () => {
|
|
350
|
-
const provider = createProvider(() => ({
|
|
351
|
-
content: [
|
|
352
|
-
{ type: "text", text: "## Goals\n- summary produced by provider" },
|
|
353
|
-
],
|
|
354
|
-
model: "mock-model",
|
|
355
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
356
|
-
stopReason: "end_turn",
|
|
357
|
-
}));
|
|
358
|
-
const manager = new ContextWindowManager({
|
|
359
|
-
provider,
|
|
360
|
-
systemPrompt: "system prompt",
|
|
361
|
-
config: makeConfig({
|
|
362
|
-
maxInputTokens: 260,
|
|
363
|
-
targetBudgetRatio: 0.59,
|
|
364
|
-
}),
|
|
365
|
-
});
|
|
366
|
-
const long = "z".repeat(220);
|
|
367
|
-
const history = [
|
|
368
|
-
message("user", `task ${long}`),
|
|
369
|
-
message("assistant", `result ${long}`),
|
|
370
|
-
message("user", `followup ${long}`),
|
|
371
|
-
];
|
|
372
|
-
|
|
373
|
-
const result = await manager.maybeCompact(history);
|
|
374
|
-
expect(result.compacted).toBe(true);
|
|
375
|
-
expect(result.summaryFailed).toBe(false);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test("serializes file blocks for summary chunks", async () => {
|
|
379
|
-
const prompts: string[] = [];
|
|
380
|
-
const provider = createProvider((messages) => {
|
|
381
|
-
for (const block of messages[0]?.content ?? []) {
|
|
382
|
-
if (block.type === "text") {
|
|
383
|
-
prompts.push(block.text);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
content: [{ type: "text", text: "## Goals\n- file summarized" }],
|
|
388
|
-
model: "mock-model",
|
|
389
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
390
|
-
stopReason: "end_turn",
|
|
391
|
-
};
|
|
392
|
-
});
|
|
393
|
-
const manager = new ContextWindowManager({
|
|
394
|
-
provider,
|
|
395
|
-
systemPrompt: "system prompt",
|
|
396
|
-
config: makeConfig({
|
|
397
|
-
maxInputTokens: 2000,
|
|
398
|
-
targetBudgetRatio: 0.4,
|
|
399
|
-
compactThreshold: 0.35,
|
|
400
|
-
}),
|
|
401
|
-
});
|
|
402
|
-
const long = "f".repeat(1500);
|
|
403
|
-
const history: Message[] = [
|
|
404
|
-
{
|
|
405
|
-
role: "user",
|
|
406
|
-
content: [
|
|
407
|
-
{
|
|
408
|
-
type: "file",
|
|
409
|
-
source: {
|
|
410
|
-
type: "base64",
|
|
411
|
-
media_type: "application/pdf",
|
|
412
|
-
filename: "spec.pdf",
|
|
413
|
-
data: "a".repeat(4096),
|
|
414
|
-
},
|
|
415
|
-
extracted_text: "Critical requirement from attached spec.",
|
|
416
|
-
},
|
|
417
|
-
],
|
|
418
|
-
},
|
|
419
|
-
message("assistant", `ack ${long}`),
|
|
420
|
-
message("user", `followup ${long}`),
|
|
421
|
-
];
|
|
422
|
-
|
|
423
|
-
const result = await manager.maybeCompact(history);
|
|
424
|
-
expect(result.compacted).toBe(true);
|
|
425
|
-
|
|
426
|
-
const combinedPrompts = prompts.join("\n");
|
|
427
|
-
expect(combinedPrompts).toContain("file: spec.pdf");
|
|
428
|
-
expect(combinedPrompts).toContain("application/pdf");
|
|
429
|
-
expect(combinedPrompts).toContain(
|
|
430
|
-
"Critical requirement from attached spec.",
|
|
431
|
-
);
|
|
432
|
-
expect(combinedPrompts).not.toContain("unknown_block");
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
test("passes image blocks to summarizer instead of text metadata", async () => {
|
|
436
|
-
const receivedBlocks: { type: string; mediaType?: string }[] = [];
|
|
437
|
-
const provider = createProvider((messages) => {
|
|
438
|
-
for (const block of messages[0]?.content ?? []) {
|
|
439
|
-
if (block.type === "image") {
|
|
440
|
-
receivedBlocks.push({
|
|
441
|
-
type: "image",
|
|
442
|
-
mediaType: (block as { source: { media_type: string } }).source
|
|
443
|
-
.media_type,
|
|
444
|
-
});
|
|
445
|
-
} else if (block.type === "text") {
|
|
446
|
-
receivedBlocks.push({ type: "text" });
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return {
|
|
450
|
-
content: [
|
|
451
|
-
{
|
|
452
|
-
type: "text",
|
|
453
|
-
text: "## Goals\n- described image: a photo of a cat",
|
|
454
|
-
},
|
|
455
|
-
],
|
|
456
|
-
model: "mock-model",
|
|
457
|
-
usage: { inputTokens: 100, outputTokens: 20 },
|
|
458
|
-
stopReason: "end_turn",
|
|
459
|
-
};
|
|
460
|
-
});
|
|
461
|
-
// Use a large enough maxInputTokens so the image fits in the summarizer
|
|
462
|
-
// budget after accounting for overhead (system prompt, scaffolding, output).
|
|
463
|
-
const manager = new ContextWindowManager({
|
|
464
|
-
provider,
|
|
465
|
-
systemPrompt: "sys",
|
|
466
|
-
config: makeConfig({
|
|
467
|
-
maxInputTokens: 5000,
|
|
468
|
-
compactThreshold: 0.3,
|
|
469
|
-
targetBudgetRatio: 0.2,
|
|
470
|
-
}),
|
|
471
|
-
});
|
|
472
|
-
const long = "x".repeat(4000);
|
|
473
|
-
const history: Message[] = [
|
|
474
|
-
{
|
|
475
|
-
role: "user",
|
|
476
|
-
content: [
|
|
477
|
-
{ type: "text", text: "look at this" },
|
|
478
|
-
{
|
|
479
|
-
type: "image",
|
|
480
|
-
source: {
|
|
481
|
-
type: "base64",
|
|
482
|
-
media_type: "image/png",
|
|
483
|
-
data: "iVBORw0KGgo=",
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
],
|
|
487
|
-
},
|
|
488
|
-
message("assistant", `a1 ${long}`),
|
|
489
|
-
message("user", `u2 ${long}`),
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
const result = await manager.maybeCompact(history);
|
|
493
|
-
expect(result.compacted).toBe(true);
|
|
494
|
-
|
|
495
|
-
// The summarizer should have received actual image blocks, not text stubs.
|
|
496
|
-
const imageBlocks = receivedBlocks.filter((b) => b.type === "image");
|
|
497
|
-
expect(imageBlocks.length).toBe(1);
|
|
498
|
-
expect(imageBlocks[0].mediaType).toBe("image/png");
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
test("passes tool_result images to summarizer", async () => {
|
|
502
|
-
const receivedImageCount = { count: 0 };
|
|
503
|
-
const provider = createProvider((messages) => {
|
|
504
|
-
for (const block of messages[0]?.content ?? []) {
|
|
505
|
-
if (block.type === "image") {
|
|
506
|
-
receivedImageCount.count++;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return {
|
|
510
|
-
content: [
|
|
511
|
-
{ type: "text", text: "## Goals\n- summarized tool output images" },
|
|
512
|
-
],
|
|
513
|
-
model: "mock-model",
|
|
514
|
-
usage: { inputTokens: 100, outputTokens: 20 },
|
|
515
|
-
stopReason: "end_turn",
|
|
516
|
-
};
|
|
517
|
-
});
|
|
518
|
-
const manager = new ContextWindowManager({
|
|
519
|
-
provider,
|
|
520
|
-
systemPrompt: "sys",
|
|
521
|
-
config: makeConfig({
|
|
522
|
-
maxInputTokens: 5000,
|
|
523
|
-
compactThreshold: 0.3,
|
|
524
|
-
targetBudgetRatio: 0.2,
|
|
525
|
-
}),
|
|
526
|
-
});
|
|
527
|
-
const long = "x".repeat(2000);
|
|
528
|
-
const history: Message[] = [
|
|
529
|
-
message("assistant", "let me read that file"),
|
|
530
|
-
{
|
|
531
|
-
role: "user",
|
|
532
|
-
content: [
|
|
533
|
-
{
|
|
534
|
-
type: "tool_result",
|
|
535
|
-
tool_use_id: "tool_1",
|
|
536
|
-
content: "file contents",
|
|
537
|
-
contentBlocks: [
|
|
538
|
-
{
|
|
539
|
-
type: "image",
|
|
540
|
-
source: {
|
|
541
|
-
type: "base64",
|
|
542
|
-
media_type: "image/jpeg",
|
|
543
|
-
data: "iVBORw0KGgo=",
|
|
544
|
-
},
|
|
545
|
-
},
|
|
546
|
-
],
|
|
547
|
-
is_error: false,
|
|
548
|
-
} as import("../providers/types.js").ToolResultContent,
|
|
549
|
-
],
|
|
550
|
-
},
|
|
551
|
-
message("user", `followup ${long}`),
|
|
552
|
-
message("assistant", `response ${long}`),
|
|
553
|
-
message("user", `final ${long}`),
|
|
554
|
-
];
|
|
555
|
-
|
|
556
|
-
const result = await manager.maybeCompact(history);
|
|
557
|
-
expect(result.compacted).toBe(true);
|
|
558
|
-
expect(receivedImageCount.count).toBe(1);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test("counts compacted persisted messages including tool-result user turns", async () => {
|
|
562
|
-
const provider = createProvider(() => ({
|
|
563
|
-
content: [{ type: "text", text: "## Goals\n- compacted summary" }],
|
|
564
|
-
model: "mock-model",
|
|
565
|
-
usage: { inputTokens: 75, outputTokens: 20 },
|
|
566
|
-
stopReason: "end_turn",
|
|
567
|
-
}));
|
|
568
|
-
const manager = new ContextWindowManager({
|
|
569
|
-
provider,
|
|
570
|
-
systemPrompt: "system prompt",
|
|
571
|
-
config: makeConfig({
|
|
572
|
-
maxInputTokens: 320,
|
|
573
|
-
targetBudgetRatio: 0.58,
|
|
574
|
-
}),
|
|
575
|
-
});
|
|
576
|
-
const long = "k".repeat(220);
|
|
577
|
-
const history: Message[] = [
|
|
578
|
-
message("user", `u1 ${long}`),
|
|
579
|
-
{
|
|
580
|
-
role: "assistant",
|
|
581
|
-
content: [
|
|
582
|
-
{
|
|
583
|
-
type: "tool_use",
|
|
584
|
-
id: "t1",
|
|
585
|
-
name: "read_file",
|
|
586
|
-
input: { path: "/tmp/a" },
|
|
587
|
-
},
|
|
588
|
-
],
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
role: "user",
|
|
592
|
-
content: [
|
|
593
|
-
{ type: "tool_result", tool_use_id: "t1", content: "contents" },
|
|
594
|
-
],
|
|
595
|
-
},
|
|
596
|
-
message("assistant", `a1 ${long}`),
|
|
597
|
-
message("user", `u2 ${long}`),
|
|
598
|
-
];
|
|
599
|
-
|
|
600
|
-
const result = await manager.maybeCompact(history);
|
|
601
|
-
expect(result.compacted).toBe(true);
|
|
602
|
-
expect(result.compactedMessages).toBe(4);
|
|
603
|
-
// Tool-result-only user messages have DB counterparts and must be
|
|
604
|
-
// counted so contextCompactedMessageCount indexes the DB correctly.
|
|
605
|
-
expect(result.compactedPersistedMessages).toBe(4);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
test("adjusts keep boundary to preserve tool_use/tool_result pairs", async () => {
|
|
609
|
-
const provider = createProvider(() => ({
|
|
610
|
-
content: [{ type: "text", text: "## Goals\n- compacted summary" }],
|
|
611
|
-
model: "mock-model",
|
|
612
|
-
usage: { inputTokens: 75, outputTokens: 20 },
|
|
613
|
-
stopReason: "end_turn",
|
|
614
|
-
}));
|
|
615
|
-
// Configure budget so compaction keeps only the last user turn,
|
|
616
|
-
// which would normally split the tool pair because the last user
|
|
617
|
-
// turn start is a mixed message (tool_result + text) whose matching
|
|
618
|
-
// tool_use lives in the preceding assistant message.
|
|
619
|
-
const manager = new ContextWindowManager({
|
|
620
|
-
provider,
|
|
621
|
-
systemPrompt: "system prompt",
|
|
622
|
-
config: makeConfig({
|
|
623
|
-
maxInputTokens: 320,
|
|
624
|
-
targetBudgetRatio: 0.58,
|
|
625
|
-
}),
|
|
626
|
-
});
|
|
627
|
-
const long = "k".repeat(220);
|
|
628
|
-
const history: Message[] = [
|
|
629
|
-
message("user", `u1 ${long}`), // index 0: old user turn (long)
|
|
630
|
-
message("assistant", `a1 ${long}`), // index 1: assistant reply (long)
|
|
631
|
-
message("user", `u2 ${long}`), // index 2: second user turn (long)
|
|
632
|
-
{
|
|
633
|
-
// index 3: assistant with tool_use
|
|
634
|
-
role: "assistant",
|
|
635
|
-
content: [
|
|
636
|
-
{
|
|
637
|
-
type: "tool_use",
|
|
638
|
-
id: "t1",
|
|
639
|
-
name: "read_file",
|
|
640
|
-
input: { path: "/tmp/a" },
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
},
|
|
644
|
-
{
|
|
645
|
-
// index 4: user with tool_result AND text (mixed = user turn start)
|
|
646
|
-
// Without adjustForToolPairs, the raw boundary would land here,
|
|
647
|
-
// orphaning the tool_result from its tool_use at index 3.
|
|
648
|
-
role: "user",
|
|
649
|
-
content: [
|
|
650
|
-
{ type: "tool_result", tool_use_id: "t1", content: "file contents" },
|
|
651
|
-
{ type: "text", text: "thanks, now continue" },
|
|
652
|
-
],
|
|
653
|
-
},
|
|
654
|
-
];
|
|
655
|
-
|
|
656
|
-
const result = await manager.maybeCompact(history);
|
|
657
|
-
expect(result.compacted).toBe(true);
|
|
658
|
-
// The kept messages must include the tool_use assistant message (index 3)
|
|
659
|
-
// and tool_result user message (index 4) as a pair, not split them.
|
|
660
|
-
// Verify no orphaned tool_result blocks exist in the kept messages.
|
|
661
|
-
const keptMessages = result.messages;
|
|
662
|
-
for (let i = 0; i < keptMessages.length; i++) {
|
|
663
|
-
const msg = keptMessages[i];
|
|
664
|
-
if (msg.role !== "user") continue;
|
|
665
|
-
for (const block of msg.content) {
|
|
666
|
-
if (block.type === "tool_result") {
|
|
667
|
-
// Every tool_result must have a matching tool_use in a preceding assistant message
|
|
668
|
-
const toolUseId = (block as { tool_use_id: string }).tool_use_id;
|
|
669
|
-
const hasMatchingToolUse = keptMessages
|
|
670
|
-
.slice(0, i)
|
|
671
|
-
.some(
|
|
672
|
-
(prev) =>
|
|
673
|
-
prev.role === "assistant" &&
|
|
674
|
-
prev.content.some(
|
|
675
|
-
(b) =>
|
|
676
|
-
b.type === "tool_use" &&
|
|
677
|
-
(b as { id: string }).id === toolUseId,
|
|
678
|
-
),
|
|
679
|
-
);
|
|
680
|
-
expect(hasMatchingToolUse).toBe(true);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
test("counts mixed tool_result+text user messages as persisted", async () => {
|
|
687
|
-
const provider = createProvider(() => ({
|
|
688
|
-
content: [{ type: "text", text: "## Goals\n- mixed summary" }],
|
|
689
|
-
model: "mock-model",
|
|
690
|
-
usage: { inputTokens: 75, outputTokens: 20 },
|
|
691
|
-
stopReason: "end_turn",
|
|
692
|
-
}));
|
|
693
|
-
const manager = new ContextWindowManager({
|
|
694
|
-
provider,
|
|
695
|
-
systemPrompt: "system prompt",
|
|
696
|
-
config: makeConfig({
|
|
697
|
-
maxInputTokens: 320,
|
|
698
|
-
targetBudgetRatio: 0.58,
|
|
699
|
-
}),
|
|
700
|
-
});
|
|
701
|
-
const long = "k".repeat(220);
|
|
702
|
-
// Simulates a merged user message (repairHistory merges consecutive same-role
|
|
703
|
-
// messages), resulting in a user turn with both tool_result and text blocks.
|
|
704
|
-
const history: Message[] = [
|
|
705
|
-
message("user", `u1 ${long}`),
|
|
706
|
-
{
|
|
707
|
-
role: "assistant",
|
|
708
|
-
content: [
|
|
709
|
-
{
|
|
710
|
-
type: "tool_use",
|
|
711
|
-
id: "t1",
|
|
712
|
-
name: "read_file",
|
|
713
|
-
input: { path: "/tmp/a" },
|
|
714
|
-
},
|
|
715
|
-
],
|
|
716
|
-
},
|
|
717
|
-
{
|
|
718
|
-
role: "user",
|
|
719
|
-
content: [
|
|
720
|
-
{ type: "tool_result", tool_use_id: "t1", content: "contents" },
|
|
721
|
-
{ type: "text", text: `follow-up question ${long}` },
|
|
722
|
-
],
|
|
723
|
-
},
|
|
724
|
-
message("assistant", `a1 ${long}`),
|
|
725
|
-
message("user", `u2 ${long}`),
|
|
726
|
-
];
|
|
727
|
-
|
|
728
|
-
const result = await manager.maybeCompact(history);
|
|
729
|
-
expect(result.compacted).toBe(true);
|
|
730
|
-
// The mixed user message should be counted as persisted (4 = u1 + mixed + a_tooluse + a1)
|
|
731
|
-
expect(result.compactedPersistedMessages).toBe(4);
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
test("returns cache-aware usage metadata for compaction summaries", async () => {
|
|
735
|
-
const rawResponse = {
|
|
736
|
-
usage: {
|
|
737
|
-
cache_creation: { ephemeral_5m_input_tokens: 120 },
|
|
738
|
-
cache_read_input_tokens: 340,
|
|
739
|
-
},
|
|
740
|
-
};
|
|
741
|
-
const provider = createProvider(() => ({
|
|
742
|
-
content: [{ type: "text", text: "## Goals\n- cache-aware summary" }],
|
|
743
|
-
model: "claude-opus-4-6",
|
|
744
|
-
usage: {
|
|
745
|
-
inputTokens: 500,
|
|
746
|
-
outputTokens: 22,
|
|
747
|
-
cacheCreationInputTokens: 120,
|
|
748
|
-
cacheReadInputTokens: 340,
|
|
749
|
-
},
|
|
750
|
-
rawResponse,
|
|
751
|
-
stopReason: "end_turn",
|
|
752
|
-
}));
|
|
753
|
-
const manager = new ContextWindowManager({
|
|
754
|
-
provider,
|
|
755
|
-
systemPrompt: "system prompt",
|
|
756
|
-
config: makeConfig({
|
|
757
|
-
maxInputTokens: 2600,
|
|
758
|
-
targetBudgetRatio: 0.63,
|
|
759
|
-
}),
|
|
760
|
-
});
|
|
761
|
-
const long = "c".repeat(5000);
|
|
762
|
-
const history: Message[] = [
|
|
763
|
-
message("user", `u1 ${long}`),
|
|
764
|
-
message("assistant", `a1 ${long}`),
|
|
765
|
-
message("user", `u2 ${long}`),
|
|
766
|
-
];
|
|
767
|
-
|
|
768
|
-
const result = await manager.maybeCompact(history);
|
|
769
|
-
|
|
770
|
-
expect(result.compacted).toBe(true);
|
|
771
|
-
expect(result.summaryCalls).toBe(1);
|
|
772
|
-
expect(result.summaryInputTokens).toBe(500);
|
|
773
|
-
expect(result.summaryCacheCreationInputTokens).toBe(120);
|
|
774
|
-
expect(result.summaryCacheReadInputTokens).toBe(340);
|
|
775
|
-
expect(result.summaryRawResponses).toEqual([rawResponse]);
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
test("does not parse user-authored summary marker text as internal summary", () => {
|
|
779
|
-
const userMessage: Message = {
|
|
780
|
-
role: "user",
|
|
781
|
-
content: [
|
|
782
|
-
{
|
|
783
|
-
type: "text",
|
|
784
|
-
text: `${CONTEXT_SUMMARY_MARKER}\nI typed this prefix myself`,
|
|
785
|
-
},
|
|
786
|
-
],
|
|
787
|
-
};
|
|
788
|
-
expect(getSummaryFromContextMessage(userMessage)).toBeNull();
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
test("skips compaction during cooldown", async () => {
|
|
792
|
-
const provider = createProvider(() => {
|
|
793
|
-
throw new Error(
|
|
794
|
-
"summarizer should not be called while cooldown skip is active",
|
|
795
|
-
);
|
|
796
|
-
});
|
|
797
|
-
const manager = new ContextWindowManager({
|
|
798
|
-
provider,
|
|
799
|
-
systemPrompt: "system prompt",
|
|
800
|
-
config: makeConfig({
|
|
801
|
-
maxInputTokens: 260,
|
|
802
|
-
targetBudgetRatio: 0.74,
|
|
803
|
-
}),
|
|
804
|
-
});
|
|
805
|
-
const long = "c".repeat(220);
|
|
806
|
-
const history: Message[] = [
|
|
807
|
-
message("user", `u1 ${long}`),
|
|
808
|
-
message("assistant", `a1 ${long}`),
|
|
809
|
-
message("user", `u2 ${long}`),
|
|
810
|
-
];
|
|
811
|
-
|
|
812
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
813
|
-
lastCompactedAt: Date.now() - 30_000,
|
|
814
|
-
});
|
|
815
|
-
expect(result.compacted).toBe(false);
|
|
816
|
-
expect(result.reason).toBe("compaction cooldown active");
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
test("ignores cooldown and compacts under severe token pressure", async () => {
|
|
820
|
-
const provider = createProvider(() => ({
|
|
821
|
-
content: [{ type: "text", text: "## Goals\n- compacted under pressure" }],
|
|
822
|
-
model: "mock-model",
|
|
823
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
824
|
-
stopReason: "end_turn",
|
|
825
|
-
}));
|
|
826
|
-
const manager = new ContextWindowManager({
|
|
827
|
-
provider,
|
|
828
|
-
systemPrompt: "system prompt",
|
|
829
|
-
config: makeConfig({
|
|
830
|
-
maxInputTokens: 320,
|
|
831
|
-
targetBudgetRatio: 0.61,
|
|
832
|
-
}),
|
|
833
|
-
});
|
|
834
|
-
const long = "p".repeat(340);
|
|
835
|
-
const history: Message[] = [
|
|
836
|
-
message("user", `u1 ${long}`),
|
|
837
|
-
message("assistant", `a1 ${long}`),
|
|
838
|
-
message("user", `u2 ${long}`),
|
|
839
|
-
message("assistant", `a2 ${long}`),
|
|
840
|
-
message("user", `u3 ${long}`),
|
|
841
|
-
];
|
|
842
|
-
|
|
843
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
844
|
-
lastCompactedAt: Date.now() - 30_000,
|
|
845
|
-
});
|
|
846
|
-
expect(result.compacted).toBe(true);
|
|
847
|
-
expect(result.reason).toBeUndefined();
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
test("force=true bypasses cooldown for context-too-large recovery", async () => {
|
|
851
|
-
const provider = createProvider(() => ({
|
|
852
|
-
content: [{ type: "text", text: "## Goals\n- forced compaction" }],
|
|
853
|
-
model: "mock-model",
|
|
854
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
855
|
-
stopReason: "end_turn",
|
|
856
|
-
}));
|
|
857
|
-
const manager = new ContextWindowManager({
|
|
858
|
-
provider,
|
|
859
|
-
systemPrompt: "system prompt",
|
|
860
|
-
config: makeConfig({
|
|
861
|
-
maxInputTokens: 260,
|
|
862
|
-
targetBudgetRatio: 0.74,
|
|
863
|
-
}),
|
|
864
|
-
});
|
|
865
|
-
const long = "c".repeat(220);
|
|
866
|
-
const history: Message[] = [
|
|
867
|
-
message("user", `u1 ${long}`),
|
|
868
|
-
message("assistant", `a1 ${long}`),
|
|
869
|
-
message("user", `u2 ${long}`),
|
|
870
|
-
];
|
|
871
|
-
|
|
872
|
-
// Same setup as the cooldown test, but with force=true — should compact.
|
|
873
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
874
|
-
lastCompactedAt: Date.now() - 30_000,
|
|
875
|
-
force: true,
|
|
876
|
-
});
|
|
877
|
-
expect(result.compacted).toBe(true);
|
|
878
|
-
expect(result.reason).toBeUndefined();
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
test("image-heavy payload is no longer underestimated as below-threshold", async () => {
|
|
882
|
-
const provider = createProvider(() => ({
|
|
883
|
-
content: [
|
|
884
|
-
{ type: "text", text: "## Goals\n- compacted image-heavy history" },
|
|
885
|
-
],
|
|
886
|
-
model: "mock-model",
|
|
887
|
-
usage: { inputTokens: 75, outputTokens: 20 },
|
|
888
|
-
stopReason: "end_turn",
|
|
889
|
-
}));
|
|
890
|
-
const manager = new ContextWindowManager({
|
|
891
|
-
provider,
|
|
892
|
-
systemPrompt: "system prompt",
|
|
893
|
-
config: makeConfig({
|
|
894
|
-
maxInputTokens: 7000,
|
|
895
|
-
targetBudgetRatio: 0.76,
|
|
896
|
-
compactThreshold: 0.8,
|
|
897
|
-
}),
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
const images = Array.from({ length: 5 }, (_, i) => ({
|
|
901
|
-
type: "image" as const,
|
|
902
|
-
source: {
|
|
903
|
-
type: "base64" as const,
|
|
904
|
-
media_type: "image/png",
|
|
905
|
-
data: `${String(i)}${"A".repeat(40_000)}`,
|
|
906
|
-
},
|
|
907
|
-
}));
|
|
908
|
-
|
|
909
|
-
const history: Message[] = [
|
|
910
|
-
{
|
|
911
|
-
role: "user",
|
|
912
|
-
content: [
|
|
913
|
-
{ type: "text", text: "Please analyze these screenshots." },
|
|
914
|
-
...images,
|
|
915
|
-
],
|
|
916
|
-
},
|
|
917
|
-
message("assistant", "Sure, uploading now."),
|
|
918
|
-
];
|
|
919
|
-
|
|
920
|
-
const result = await manager.maybeCompact(history);
|
|
921
|
-
expect(result.reason).not.toBe("below compaction threshold");
|
|
922
|
-
|
|
923
|
-
// Sanity check for this repro: counting raw base64 as text would exceed threshold.
|
|
924
|
-
const rawBase64Chars = images.reduce(
|
|
925
|
-
(sum, img) => sum + img.source.data.length,
|
|
926
|
-
0,
|
|
927
|
-
);
|
|
928
|
-
const rawBase64TokenEquivalent = estimateTextTokens(
|
|
929
|
-
"A".repeat(rawBase64Chars),
|
|
930
|
-
);
|
|
931
|
-
expect(rawBase64TokenEquivalent).toBeGreaterThan(result.thresholdTokens);
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
test("minKeepRecentUserTurns: 0 compacts all messages into summary only", async () => {
|
|
935
|
-
const provider = createProvider(() => ({
|
|
936
|
-
content: [{ type: "text", text: "## Goals\n- emergency summary" }],
|
|
937
|
-
model: "mock-model",
|
|
938
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
939
|
-
stopReason: "end_turn",
|
|
940
|
-
}));
|
|
941
|
-
const manager = new ContextWindowManager({
|
|
942
|
-
provider,
|
|
943
|
-
systemPrompt: "system prompt",
|
|
944
|
-
config: makeConfig({
|
|
945
|
-
maxInputTokens: 260,
|
|
946
|
-
targetBudgetRatio: 0.28,
|
|
947
|
-
}),
|
|
948
|
-
});
|
|
949
|
-
const long = "e".repeat(220);
|
|
950
|
-
const history: Message[] = [
|
|
951
|
-
message("user", `u1 ${long}`),
|
|
952
|
-
message("assistant", `a1 ${long}`),
|
|
953
|
-
message("user", `u2 ${long}`),
|
|
954
|
-
];
|
|
955
|
-
|
|
956
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
957
|
-
force: true,
|
|
958
|
-
minKeepRecentUserTurns: 0,
|
|
959
|
-
});
|
|
960
|
-
expect(result.compacted).toBe(true);
|
|
961
|
-
// With minKeepRecentUserTurns=0 and a tight target budget,
|
|
962
|
-
// pickKeepBoundary drops keepTurns all the way to 0.
|
|
963
|
-
// All three messages are compacted into a single summary message.
|
|
964
|
-
expect(result.compactedMessages).toBe(3);
|
|
965
|
-
expect(result.messages).toHaveLength(1);
|
|
966
|
-
expect(getSummaryFromContextMessage(result.messages[0])).toContain(
|
|
967
|
-
"emergency summary",
|
|
968
|
-
);
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
test("force compaction with loose target override still summarizes persisted messages", async () => {
|
|
972
|
-
// `pickKeepBoundary` clamps `targetInputTokensOverride` to
|
|
973
|
-
// `config.targetInputTokens`, so a loose override cannot
|
|
974
|
-
// short-circuit summarization into the truncate-only early-exit.
|
|
975
|
-
|
|
976
|
-
let summaryCalls = 0;
|
|
977
|
-
const provider = createProvider(() => {
|
|
978
|
-
summaryCalls += 1;
|
|
979
|
-
return {
|
|
980
|
-
content: [{ type: "text", text: "## Goals\n- real summary" }],
|
|
981
|
-
model: "mock-model",
|
|
982
|
-
usage: { inputTokens: 80, outputTokens: 20 },
|
|
983
|
-
stopReason: "end_turn",
|
|
984
|
-
};
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
// Scaled from prod (max 200k → 1000) preserving key ratios: the
|
|
988
|
-
// loose override (~0.85×max) is ~17× the post-compaction target
|
|
989
|
-
// (~0.05×max), so history between the two exercises the clamp.
|
|
990
|
-
const manager = new ContextWindowManager({
|
|
991
|
-
provider,
|
|
992
|
-
systemPrompt: "system prompt",
|
|
993
|
-
config: makeConfig({
|
|
994
|
-
maxInputTokens: 1000,
|
|
995
|
-
targetBudgetRatio: 0.1,
|
|
996
|
-
summaryBudgetRatio: 0.05,
|
|
997
|
-
compactThreshold: 0.3,
|
|
998
|
-
}),
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
// History in the "no-op zone": above threshold (300), below override (850).
|
|
1002
|
-
const long = "x".repeat(180);
|
|
1003
|
-
const history: Message[] = [
|
|
1004
|
-
message("user", `u1 ${long}`),
|
|
1005
|
-
message("assistant", `a1 ${long}`),
|
|
1006
|
-
message("user", `u2 ${long}`),
|
|
1007
|
-
message("assistant", `a2 ${long}`),
|
|
1008
|
-
message("user", `u3 ${long}`),
|
|
1009
|
-
message("assistant", `a3 ${long}`),
|
|
1010
|
-
message("user", `u4 ${long}`),
|
|
1011
|
-
message("assistant", `a4 ${long}`),
|
|
1012
|
-
message("user", `u5 ${long}`),
|
|
1013
|
-
];
|
|
1014
|
-
|
|
1015
|
-
const preflightBudgetAnalog = Math.floor(1000 * 0.85);
|
|
1016
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
1017
|
-
force: true,
|
|
1018
|
-
targetInputTokensOverride: preflightBudgetAnalog,
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
// Guard: we're actually above the compact threshold.
|
|
1022
|
-
expect(result.previousEstimatedInputTokens).toBeGreaterThan(
|
|
1023
|
-
result.thresholdTokens,
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
|
-
// A real summarization happened (not the truncate-only no-op).
|
|
1027
|
-
expect(result.compactedPersistedMessages).toBeGreaterThan(0);
|
|
1028
|
-
expect(summaryCalls).toBeGreaterThan(0);
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
test("force=true compacts below minFloor when a kept turn exceeds target", async () => {
|
|
1032
|
-
// A giant paste in the last user turn means minFloor=1 alone exceeds target.
|
|
1033
|
-
// Under force, pickKeepBoundary should walk keepTurns below minFloor (down to
|
|
1034
|
-
// 0) so the huge block falls into the compacted region and gets summarized
|
|
1035
|
-
// instead of being kept at full size.
|
|
1036
|
-
const provider = createProvider(() => ({
|
|
1037
|
-
content: [{ type: "text", text: "## Goals\n- compressed large paste" }],
|
|
1038
|
-
model: "mock-model",
|
|
1039
|
-
usage: { inputTokens: 120, outputTokens: 20 },
|
|
1040
|
-
stopReason: "end_turn",
|
|
1041
|
-
}));
|
|
1042
|
-
const manager = new ContextWindowManager({
|
|
1043
|
-
provider,
|
|
1044
|
-
systemPrompt: "system prompt",
|
|
1045
|
-
config: makeConfig({ maxInputTokens: 600, targetBudgetRatio: 0.2 }),
|
|
1046
|
-
});
|
|
1047
|
-
const hugePaste = "p".repeat(4000); // ~1000 tokens, well above targetInputTokens
|
|
1048
|
-
const history: Message[] = [
|
|
1049
|
-
message("user", "u1 small"),
|
|
1050
|
-
message("assistant", "a1 small"),
|
|
1051
|
-
message("user", `u2 ${hugePaste}`),
|
|
1052
|
-
];
|
|
1053
|
-
|
|
1054
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
1055
|
-
force: true,
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
expect(result.compacted).toBe(true);
|
|
1059
|
-
// With force=true the kept region is empty; all turns including the oversized
|
|
1060
|
-
// paste were summarized, so the compacted result is just the summary.
|
|
1061
|
-
expect(result.messages).toHaveLength(1);
|
|
1062
|
-
expect(result.compactedMessages).toBe(history.length);
|
|
1063
|
-
expect(getSummaryFromContextMessage(result.messages[0])).toContain(
|
|
1064
|
-
"compressed large paste",
|
|
1065
|
-
);
|
|
1066
|
-
expect(result.estimatedInputTokens).toBeLessThan(
|
|
1067
|
-
result.previousEstimatedInputTokens,
|
|
1068
|
-
);
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
test("force=false honors minFloor even when the kept turn exceeds target", async () => {
|
|
1072
|
-
// Same oversized paste, but without force the algorithm must preserve the
|
|
1073
|
-
// minFloor=1 recent turn (auto mid-loop compaction needs the in-flight turn
|
|
1074
|
-
// intact). Anything compactable before the floor still gets summarized.
|
|
1075
|
-
const provider = createProvider(() => ({
|
|
1076
|
-
content: [{ type: "text", text: "## Goals\n- summary" }],
|
|
1077
|
-
model: "mock-model",
|
|
1078
|
-
usage: { inputTokens: 60, outputTokens: 10 },
|
|
1079
|
-
stopReason: "end_turn",
|
|
1080
|
-
}));
|
|
1081
|
-
const manager = new ContextWindowManager({
|
|
1082
|
-
provider,
|
|
1083
|
-
systemPrompt: "system prompt",
|
|
1084
|
-
config: makeConfig({ maxInputTokens: 600, targetBudgetRatio: 0.2 }),
|
|
1085
|
-
});
|
|
1086
|
-
const hugePaste = "p".repeat(4000);
|
|
1087
|
-
const history: Message[] = [
|
|
1088
|
-
message("user", "u1 small"),
|
|
1089
|
-
message("assistant", "a1 small"),
|
|
1090
|
-
message("user", "u2 small"),
|
|
1091
|
-
message("assistant", "a2 small"),
|
|
1092
|
-
message("user", `u3 ${hugePaste}`),
|
|
1093
|
-
];
|
|
1094
|
-
|
|
1095
|
-
const result = await manager.maybeCompact(history);
|
|
1096
|
-
|
|
1097
|
-
expect(result.compacted).toBe(true);
|
|
1098
|
-
// The oversized last user turn is retained verbatim; the kept array starts
|
|
1099
|
-
// with the summary followed by the messages from that turn onward.
|
|
1100
|
-
const lastUser = result.messages
|
|
1101
|
-
.filter((m) => m.role === "user")
|
|
1102
|
-
.map((m) => (m.content[0].type === "text" ? m.content[0].text : ""))
|
|
1103
|
-
.find((t) => t.startsWith("u3 "));
|
|
1104
|
-
expect(lastUser).toBeDefined();
|
|
1105
|
-
expect(lastUser!.length).toBeGreaterThan(hugePaste.length);
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
test("shouldCompact returns needed=false with estimatedTokens when below threshold", () => {
|
|
1109
|
-
const provider = createProvider(() => {
|
|
1110
|
-
throw new Error("should not be called");
|
|
1111
|
-
});
|
|
1112
|
-
const manager = new ContextWindowManager({
|
|
1113
|
-
provider,
|
|
1114
|
-
systemPrompt: "system prompt",
|
|
1115
|
-
config: makeConfig(),
|
|
1116
|
-
});
|
|
1117
|
-
const history = [message("user", "hello"), message("assistant", "hi")];
|
|
1118
|
-
const result = manager.shouldCompact(history);
|
|
1119
|
-
expect(result.needed).toBe(false);
|
|
1120
|
-
expect(result.estimatedTokens).toBeGreaterThan(0);
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
test("shouldCompact returns needed=true with estimatedTokens when above threshold", () => {
|
|
1124
|
-
const provider = createProvider(() => {
|
|
1125
|
-
throw new Error("should not be called");
|
|
1126
|
-
});
|
|
1127
|
-
const manager = new ContextWindowManager({
|
|
1128
|
-
provider,
|
|
1129
|
-
systemPrompt: "system prompt",
|
|
1130
|
-
config: makeConfig(),
|
|
1131
|
-
});
|
|
1132
|
-
const long = "x".repeat(240);
|
|
1133
|
-
const history: Message[] = [
|
|
1134
|
-
message("user", `u1 ${long}`),
|
|
1135
|
-
message("assistant", `a1 ${long}`),
|
|
1136
|
-
message("user", `u2 ${long}`),
|
|
1137
|
-
message("assistant", `a2 ${long}`),
|
|
1138
|
-
message("user", `u3 ${long}`),
|
|
1139
|
-
message("assistant", `a3 ${long}`),
|
|
1140
|
-
];
|
|
1141
|
-
const result = manager.shouldCompact(history);
|
|
1142
|
-
expect(result.needed).toBe(true);
|
|
1143
|
-
expect(result.estimatedTokens).toBeGreaterThan(0);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
test("shouldCompact returns needed=false with zero estimatedTokens when disabled", () => {
|
|
1147
|
-
const provider = createProvider(() => {
|
|
1148
|
-
throw new Error("should not be called");
|
|
1149
|
-
});
|
|
1150
|
-
const long = "x".repeat(240);
|
|
1151
|
-
const manager = new ContextWindowManager({
|
|
1152
|
-
provider,
|
|
1153
|
-
systemPrompt: "system prompt",
|
|
1154
|
-
config: makeConfig({ enabled: false }),
|
|
1155
|
-
});
|
|
1156
|
-
const history: Message[] = [
|
|
1157
|
-
message("user", `u1 ${long}`),
|
|
1158
|
-
message("assistant", `a1 ${long}`),
|
|
1159
|
-
message("user", `u2 ${long}`),
|
|
1160
|
-
message("assistant", `a2 ${long}`),
|
|
1161
|
-
];
|
|
1162
|
-
const result = manager.shouldCompact(history);
|
|
1163
|
-
expect(result.needed).toBe(false);
|
|
1164
|
-
expect(result.estimatedTokens).toBe(0);
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
test("truncates tool results in kept turns to preserve more conversation", async () => {
|
|
1168
|
-
const provider = createProvider(() => ({
|
|
1169
|
-
content: [{ type: "text", text: "## Goals\n- truncation summary" }],
|
|
1170
|
-
model: "mock-model",
|
|
1171
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1172
|
-
stopReason: "end_turn",
|
|
1173
|
-
}));
|
|
1174
|
-
// Budget is tight enough that full 8K tool results would force dropping turns,
|
|
1175
|
-
// but truncated results (≤6K chars) should allow more turns to be kept.
|
|
1176
|
-
const config = makeConfig({
|
|
1177
|
-
maxInputTokens: 4000,
|
|
1178
|
-
targetBudgetRatio: 0.7,
|
|
1179
|
-
});
|
|
1180
|
-
const manager = new ContextWindowManager({
|
|
1181
|
-
provider,
|
|
1182
|
-
systemPrompt: "system prompt",
|
|
1183
|
-
config,
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
const largeToolResult = "x".repeat(8000);
|
|
1187
|
-
const history: Message[] = [
|
|
1188
|
-
message("user", "u1"),
|
|
1189
|
-
{
|
|
1190
|
-
role: "assistant",
|
|
1191
|
-
content: [
|
|
1192
|
-
{
|
|
1193
|
-
type: "tool_use",
|
|
1194
|
-
id: "t1",
|
|
1195
|
-
name: "read_file",
|
|
1196
|
-
input: { path: "/tmp/a" },
|
|
1197
|
-
},
|
|
1198
|
-
],
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
role: "user",
|
|
1202
|
-
content: [
|
|
1203
|
-
{
|
|
1204
|
-
type: "tool_result",
|
|
1205
|
-
tool_use_id: "t1",
|
|
1206
|
-
content: largeToolResult,
|
|
1207
|
-
},
|
|
1208
|
-
],
|
|
1209
|
-
},
|
|
1210
|
-
message("assistant", "a1"),
|
|
1211
|
-
message("user", "u2"),
|
|
1212
|
-
{
|
|
1213
|
-
role: "assistant",
|
|
1214
|
-
content: [
|
|
1215
|
-
{
|
|
1216
|
-
type: "tool_use",
|
|
1217
|
-
id: "t2",
|
|
1218
|
-
name: "read_file",
|
|
1219
|
-
input: { path: "/tmp/b" },
|
|
1220
|
-
},
|
|
1221
|
-
],
|
|
1222
|
-
},
|
|
1223
|
-
{
|
|
1224
|
-
role: "user",
|
|
1225
|
-
content: [
|
|
1226
|
-
{
|
|
1227
|
-
type: "tool_result",
|
|
1228
|
-
tool_use_id: "t2",
|
|
1229
|
-
content: largeToolResult,
|
|
1230
|
-
},
|
|
1231
|
-
],
|
|
1232
|
-
},
|
|
1233
|
-
message("assistant", "a2"),
|
|
1234
|
-
message("user", "u3"),
|
|
1235
|
-
{
|
|
1236
|
-
role: "assistant",
|
|
1237
|
-
content: [
|
|
1238
|
-
{
|
|
1239
|
-
type: "tool_use",
|
|
1240
|
-
id: "t3",
|
|
1241
|
-
name: "read_file",
|
|
1242
|
-
input: { path: "/tmp/c" },
|
|
1243
|
-
},
|
|
1244
|
-
],
|
|
1245
|
-
},
|
|
1246
|
-
{
|
|
1247
|
-
role: "user",
|
|
1248
|
-
content: [
|
|
1249
|
-
{
|
|
1250
|
-
type: "tool_result",
|
|
1251
|
-
tool_use_id: "t3",
|
|
1252
|
-
content: largeToolResult,
|
|
1253
|
-
},
|
|
1254
|
-
],
|
|
1255
|
-
},
|
|
1256
|
-
message("assistant", "a3"),
|
|
1257
|
-
message("user", "u4"),
|
|
1258
|
-
message("assistant", "a4"),
|
|
1259
|
-
];
|
|
1260
|
-
|
|
1261
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
1262
|
-
force: true,
|
|
1263
|
-
});
|
|
1264
|
-
expect(result.compacted).toBe(true);
|
|
1265
|
-
|
|
1266
|
-
// Verify tool results in output are truncated (should be < 8K chars each).
|
|
1267
|
-
for (const msg of result.messages) {
|
|
1268
|
-
for (const block of msg.content) {
|
|
1269
|
-
if (block.type === "tool_result") {
|
|
1270
|
-
expect(block.content.length).toBeLessThan(8000);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
test("targetInputTokensOverride reduces retained turns beyond normal compaction", async () => {
|
|
1277
|
-
const provider = createProvider(() => ({
|
|
1278
|
-
content: [{ type: "text", text: "## Goals\n- tight fit summary" }],
|
|
1279
|
-
model: "mock-model",
|
|
1280
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1281
|
-
stopReason: "end_turn",
|
|
1282
|
-
}));
|
|
1283
|
-
|
|
1284
|
-
// Use generous default target so normal compaction would keep all 3 user turns.
|
|
1285
|
-
const config = makeConfig({
|
|
1286
|
-
maxInputTokens: 1200,
|
|
1287
|
-
targetBudgetRatio: 0.88,
|
|
1288
|
-
});
|
|
1289
|
-
const long = "t".repeat(220);
|
|
1290
|
-
const history: Message[] = [
|
|
1291
|
-
message("user", `u1 ${long}`),
|
|
1292
|
-
message("assistant", `a1 ${long}`),
|
|
1293
|
-
message("user", `u2 ${long}`),
|
|
1294
|
-
message("assistant", `a2 ${long}`),
|
|
1295
|
-
message("user", `u3 ${long}`),
|
|
1296
|
-
message("assistant", `a3 ${long}`),
|
|
1297
|
-
];
|
|
1298
|
-
|
|
1299
|
-
// Without override: normal compaction keeps more turns.
|
|
1300
|
-
const normalManager = new ContextWindowManager({
|
|
1301
|
-
provider,
|
|
1302
|
-
systemPrompt: "system prompt",
|
|
1303
|
-
config,
|
|
1304
|
-
});
|
|
1305
|
-
const normalResult = await normalManager.maybeCompact(history, undefined, {
|
|
1306
|
-
force: true,
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
// With a very tight override target: should keep fewer turns.
|
|
1310
|
-
const tightManager = new ContextWindowManager({
|
|
1311
|
-
provider,
|
|
1312
|
-
systemPrompt: "system prompt",
|
|
1313
|
-
config,
|
|
1314
|
-
});
|
|
1315
|
-
const tightResult = await tightManager.maybeCompact(history, undefined, {
|
|
1316
|
-
force: true,
|
|
1317
|
-
targetInputTokensOverride: 80,
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
expect(tightResult.compacted).toBe(true);
|
|
1321
|
-
// The tight override should compact more messages than normal.
|
|
1322
|
-
expect(tightResult.compactedMessages).toBeGreaterThan(
|
|
1323
|
-
normalResult.compactedMessages,
|
|
1324
|
-
);
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
test("subtracts summaryOffset only when summary at index 0 was injected from parent", async () => {
|
|
1328
|
-
const provider = createProvider(() => ({
|
|
1329
|
-
content: [{ type: "text", text: "## Goals\n- new child summary" }],
|
|
1330
|
-
model: "mock-model",
|
|
1331
|
-
usage: { inputTokens: 75, outputTokens: 20 },
|
|
1332
|
-
stopReason: "end_turn",
|
|
1333
|
-
}));
|
|
1334
|
-
const manager = new ContextWindowManager({
|
|
1335
|
-
provider,
|
|
1336
|
-
systemPrompt: "system prompt",
|
|
1337
|
-
config: makeConfig({
|
|
1338
|
-
maxInputTokens: 320,
|
|
1339
|
-
targetBudgetRatio: 0.58,
|
|
1340
|
-
}),
|
|
1341
|
-
});
|
|
1342
|
-
const long = "k".repeat(220);
|
|
1343
|
-
// Parent-injected summary at index 0, plus 2 injected non-persisted
|
|
1344
|
-
// messages, plus 3 child-persisted messages. nonPersistedPrefixCount
|
|
1345
|
-
// includes the summary (set by injectInheritedContext).
|
|
1346
|
-
const history: Message[] = [
|
|
1347
|
-
createContextSummaryMessage("parent summary"),
|
|
1348
|
-
message("user", `injected-u ${long}`),
|
|
1349
|
-
message("assistant", `injected-a ${long}`),
|
|
1350
|
-
message("user", `persisted-u1 ${long}`),
|
|
1351
|
-
message("assistant", `persisted-a1 ${long}`),
|
|
1352
|
-
message("user", `persisted-u2 ${long}`),
|
|
1353
|
-
];
|
|
1354
|
-
manager.nonPersistedPrefixCount = 3;
|
|
1355
|
-
manager.summaryIsInjected = true;
|
|
1356
|
-
|
|
1357
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
1358
|
-
force: true,
|
|
1359
|
-
});
|
|
1360
|
-
expect(result.compacted).toBe(true);
|
|
1361
|
-
// 4 messages compacted (2 injected + 2 child-persisted), but only the
|
|
1362
|
-
// 2 child-persisted ones count as DB-persisted.
|
|
1363
|
-
expect(result.compactedMessages).toBe(4);
|
|
1364
|
-
expect(result.compactedPersistedMessages).toBe(2);
|
|
1365
|
-
// Flag clears and prefix drains (both injected messages + summary slot).
|
|
1366
|
-
expect(manager.summaryIsInjected).toBe(false);
|
|
1367
|
-
expect(manager.nonPersistedPrefixCount).toBe(0);
|
|
1368
|
-
});
|
|
1369
|
-
|
|
1370
|
-
test("summary system prompt instructs verbatim thread-anchor preservation", async () => {
|
|
1371
|
-
const capturedSystemPrompts: (string | undefined)[] = [];
|
|
1372
|
-
const provider: Provider = {
|
|
1373
|
-
name: "mock",
|
|
1374
|
-
async sendMessage(
|
|
1375
|
-
_messages: Message[],
|
|
1376
|
-
_tools,
|
|
1377
|
-
systemPrompt,
|
|
1378
|
-
): Promise<ProviderResponse> {
|
|
1379
|
-
capturedSystemPrompts.push(systemPrompt);
|
|
1380
|
-
return {
|
|
1381
|
-
content: [
|
|
1382
|
-
{
|
|
1383
|
-
type: "text",
|
|
1384
|
-
text: "## Goals\n- preserved thread parent verbatim",
|
|
1385
|
-
},
|
|
1386
|
-
],
|
|
1387
|
-
model: "mock-model",
|
|
1388
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1389
|
-
stopReason: "end_turn",
|
|
1390
|
-
};
|
|
1391
|
-
},
|
|
1392
|
-
};
|
|
1393
|
-
const manager = new ContextWindowManager({
|
|
1394
|
-
provider,
|
|
1395
|
-
systemPrompt: "system prompt",
|
|
1396
|
-
config: makeConfig({ maxInputTokens: 600 }),
|
|
1397
|
-
});
|
|
1398
|
-
const long = "x".repeat(240);
|
|
1399
|
-
// Simulate a Slack-style transcript where an old user "thread parent"
|
|
1400
|
-
// message is about to be compacted while a later reply survives in the
|
|
1401
|
-
// retained tail. The clause being asserted instructs the summarizer to
|
|
1402
|
-
// preserve that parent verbatim — we cannot verify the model's behavior
|
|
1403
|
-
// here (the provider is a stub), so we instead assert the clause itself
|
|
1404
|
-
// reaches the summarizer.
|
|
1405
|
-
const history: Message[] = [
|
|
1406
|
-
message("user", `parent: kickoff plan ${long}`),
|
|
1407
|
-
message("assistant", `a1 ${long}`),
|
|
1408
|
-
message("user", `u2 ${long}`),
|
|
1409
|
-
message("assistant", `a2 ${long}`),
|
|
1410
|
-
message("user", `reply-in-thread ${long}`),
|
|
1411
|
-
message("assistant", `a3 ${long}`),
|
|
1412
|
-
];
|
|
1413
|
-
|
|
1414
|
-
const result = await manager.maybeCompact(history);
|
|
1415
|
-
expect(result.compacted).toBe(true);
|
|
1416
|
-
expect(capturedSystemPrompts.length).toBeGreaterThan(0);
|
|
1417
|
-
const seenPrompt = capturedSystemPrompts[0];
|
|
1418
|
-
expect(seenPrompt).toBeDefined();
|
|
1419
|
-
expect(seenPrompt).toContain("Thread anchors");
|
|
1420
|
-
expect(seenPrompt).toContain("verbatim");
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
test("summary prompt lists retained-tail thread-reply references", async () => {
|
|
1424
|
-
const capturedMessages: Message[][] = [];
|
|
1425
|
-
const provider: Provider = {
|
|
1426
|
-
name: "mock",
|
|
1427
|
-
async sendMessage(messages: Message[]): Promise<ProviderResponse> {
|
|
1428
|
-
capturedMessages.push(messages);
|
|
1429
|
-
return {
|
|
1430
|
-
content: [{ type: "text", text: "## Goals\n- ok" }],
|
|
1431
|
-
model: "mock-model",
|
|
1432
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1433
|
-
stopReason: "end_turn",
|
|
1434
|
-
};
|
|
1435
|
-
},
|
|
1436
|
-
};
|
|
1437
|
-
const manager = new ContextWindowManager({
|
|
1438
|
-
provider,
|
|
1439
|
-
systemPrompt: "system prompt",
|
|
1440
|
-
config: makeConfig({ maxInputTokens: 600 }),
|
|
1441
|
-
});
|
|
1442
|
-
const long = "x".repeat(240);
|
|
1443
|
-
// Compactable region ends before the retained tail, which contains a
|
|
1444
|
-
// Slack-style reply line that cites its parent via `→ M1a2b3c`. The
|
|
1445
|
-
// summary prompt must surface that reference so the Thread-anchors
|
|
1446
|
-
// instruction has something to act on.
|
|
1447
|
-
const history: Message[] = [
|
|
1448
|
-
message("user", `[11/14/23 14:25 @alice]: parent kickoff ${long}`),
|
|
1449
|
-
message("assistant", `a1 ${long}`),
|
|
1450
|
-
message("user", `u2 ${long}`),
|
|
1451
|
-
message("assistant", `a2 ${long}`),
|
|
1452
|
-
message("user", `[11/14/23 14:28 @bob → M1a2b3c]: reply ${long}`),
|
|
1453
|
-
message("assistant", `a3 ${long}`),
|
|
1454
|
-
];
|
|
1455
|
-
|
|
1456
|
-
const result = await manager.maybeCompact(history);
|
|
1457
|
-
expect(result.compacted).toBe(true);
|
|
1458
|
-
expect(capturedMessages.length).toBeGreaterThan(0);
|
|
1459
|
-
const userPromptText = capturedMessages[0]
|
|
1460
|
-
.flatMap((m) => m.content)
|
|
1461
|
-
.filter(
|
|
1462
|
-
(b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
|
|
1463
|
-
)
|
|
1464
|
-
.map((b) => b.text)
|
|
1465
|
-
.join("\n");
|
|
1466
|
-
expect(userPromptText).toContain("### Retained Thread References");
|
|
1467
|
-
expect(userPromptText).toContain("→ M1a2b3c");
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
test("summary prompt lists retained-tail thread-reply references for edited replies", async () => {
|
|
1471
|
-
const capturedMessages: Message[][] = [];
|
|
1472
|
-
const provider: Provider = {
|
|
1473
|
-
name: "mock",
|
|
1474
|
-
async sendMessage(messages: Message[]): Promise<ProviderResponse> {
|
|
1475
|
-
capturedMessages.push(messages);
|
|
1476
|
-
return {
|
|
1477
|
-
content: [{ type: "text", text: "## Goals\n- ok" }],
|
|
1478
|
-
model: "mock-model",
|
|
1479
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1480
|
-
stopReason: "end_turn",
|
|
1481
|
-
};
|
|
1482
|
-
},
|
|
1483
|
-
};
|
|
1484
|
-
const manager = new ContextWindowManager({
|
|
1485
|
-
provider,
|
|
1486
|
-
systemPrompt: "system prompt",
|
|
1487
|
-
config: makeConfig({ maxInputTokens: 600 }),
|
|
1488
|
-
});
|
|
1489
|
-
const long = "x".repeat(240);
|
|
1490
|
-
// An edited reply renders with `, edited …` between the parent alias and
|
|
1491
|
-
// the closing bracket: `→ Mxxxxxx, edited MM/DD/YY HH:MM]`. The regex
|
|
1492
|
-
// must still flag these lines so retention works for edited replies.
|
|
1493
|
-
const history: Message[] = [
|
|
1494
|
-
message("user", `[11/14/23 14:25 @alice]: parent kickoff ${long}`),
|
|
1495
|
-
message("assistant", `a1 ${long}`),
|
|
1496
|
-
message("user", `u2 ${long}`),
|
|
1497
|
-
message("assistant", `a2 ${long}`),
|
|
1498
|
-
message(
|
|
1499
|
-
"user",
|
|
1500
|
-
`[11/14/23 14:28 @bob → M1a2b3c, edited 11/14/23 14:32]: reply ${long}`,
|
|
1501
|
-
),
|
|
1502
|
-
message("assistant", `a3 ${long}`),
|
|
1503
|
-
];
|
|
1504
|
-
|
|
1505
|
-
const result = await manager.maybeCompact(history);
|
|
1506
|
-
expect(result.compacted).toBe(true);
|
|
1507
|
-
expect(capturedMessages.length).toBeGreaterThan(0);
|
|
1508
|
-
const userPromptText = capturedMessages[0]
|
|
1509
|
-
.flatMap((m) => m.content)
|
|
1510
|
-
.filter(
|
|
1511
|
-
(b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
|
|
1512
|
-
)
|
|
1513
|
-
.map((b) => b.text)
|
|
1514
|
-
.join("\n");
|
|
1515
|
-
expect(userPromptText).toContain("### Retained Thread References");
|
|
1516
|
-
expect(userPromptText).toContain("→ M1a2b3c, edited 11/14/23 14:32");
|
|
1517
|
-
});
|
|
1518
|
-
|
|
1519
|
-
test("summary prompt omits retained references when retained tail has no thread markers", async () => {
|
|
1520
|
-
const capturedMessages: Message[][] = [];
|
|
1521
|
-
const provider: Provider = {
|
|
1522
|
-
name: "mock",
|
|
1523
|
-
async sendMessage(messages: Message[]): Promise<ProviderResponse> {
|
|
1524
|
-
capturedMessages.push(messages);
|
|
1525
|
-
return {
|
|
1526
|
-
content: [{ type: "text", text: "## Goals\n- ok" }],
|
|
1527
|
-
model: "mock-model",
|
|
1528
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1529
|
-
stopReason: "end_turn",
|
|
1530
|
-
};
|
|
1531
|
-
},
|
|
1532
|
-
};
|
|
1533
|
-
const manager = new ContextWindowManager({
|
|
1534
|
-
provider,
|
|
1535
|
-
systemPrompt: "system prompt",
|
|
1536
|
-
config: makeConfig({ maxInputTokens: 600 }),
|
|
1537
|
-
});
|
|
1538
|
-
const long = "x".repeat(240);
|
|
1539
|
-
const history: Message[] = [
|
|
1540
|
-
message("user", `u1 ${long}`),
|
|
1541
|
-
message("assistant", `a1 ${long}`),
|
|
1542
|
-
message("user", `u2 ${long}`),
|
|
1543
|
-
message("assistant", `a2 ${long}`),
|
|
1544
|
-
message("user", `u3 ${long}`),
|
|
1545
|
-
message("assistant", `a3 ${long}`),
|
|
1546
|
-
];
|
|
1547
|
-
|
|
1548
|
-
const result = await manager.maybeCompact(history);
|
|
1549
|
-
expect(result.compacted).toBe(true);
|
|
1550
|
-
const userPromptText = capturedMessages[0]
|
|
1551
|
-
.flatMap((m) => m.content)
|
|
1552
|
-
.filter(
|
|
1553
|
-
(b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
|
|
1554
|
-
)
|
|
1555
|
-
.map((b) => b.text)
|
|
1556
|
-
.join("\n");
|
|
1557
|
-
expect(userPromptText).not.toContain("### Retained Thread References");
|
|
1558
|
-
expect(userPromptText).not.toMatch(/→ M[0-9a-f]{6}]/);
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
test("does not subtract summaryOffset when summary at index 0 is child-owned from prior compaction", async () => {
|
|
1562
|
-
const provider = createProvider(() => ({
|
|
1563
|
-
content: [{ type: "text", text: "## Goals\n- next child summary" }],
|
|
1564
|
-
model: "mock-model",
|
|
1565
|
-
usage: { inputTokens: 75, outputTokens: 20 },
|
|
1566
|
-
stopReason: "end_turn",
|
|
1567
|
-
}));
|
|
1568
|
-
const manager = new ContextWindowManager({
|
|
1569
|
-
provider,
|
|
1570
|
-
systemPrompt: "system prompt",
|
|
1571
|
-
config: makeConfig({
|
|
1572
|
-
maxInputTokens: 320,
|
|
1573
|
-
targetBudgetRatio: 0.58,
|
|
1574
|
-
}),
|
|
1575
|
-
});
|
|
1576
|
-
const long = "k".repeat(220);
|
|
1577
|
-
// Post-first-compaction state: child-owned summary at index 0, 2
|
|
1578
|
-
// still-injected messages that survived the first compaction's keep
|
|
1579
|
-
// region, 3 child-persisted messages. nonPersistedPrefixCount reflects
|
|
1580
|
-
// only the 2 remaining injected messages — the summary slot was already
|
|
1581
|
-
// consumed when the flag-gated decrement ran on the prior compaction.
|
|
1582
|
-
const history: Message[] = [
|
|
1583
|
-
createContextSummaryMessage("prior child summary"),
|
|
1584
|
-
message("user", `injected-u ${long}`),
|
|
1585
|
-
message("assistant", `injected-a ${long}`),
|
|
1586
|
-
message("user", `persisted-u1 ${long}`),
|
|
1587
|
-
message("assistant", `persisted-a1 ${long}`),
|
|
1588
|
-
message("user", `persisted-u2 ${long}`),
|
|
1589
|
-
];
|
|
1590
|
-
manager.nonPersistedPrefixCount = 2;
|
|
1591
|
-
manager.summaryIsInjected = false;
|
|
1592
|
-
|
|
1593
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
1594
|
-
force: true,
|
|
1595
|
-
});
|
|
1596
|
-
expect(result.compacted).toBe(true);
|
|
1597
|
-
expect(result.compactedMessages).toBe(4);
|
|
1598
|
-
// Regression guard: without the flag gate, the subtraction from the
|
|
1599
|
-
// #24353 fix would double-apply here (nonPersistedPrefixCount - 1),
|
|
1600
|
-
// undercounting injectedInCompactable and inflating
|
|
1601
|
-
// compactedPersistedMessages by 1 (to 3).
|
|
1602
|
-
expect(result.compactedPersistedMessages).toBe(2);
|
|
1603
|
-
expect(manager.nonPersistedPrefixCount).toBe(0);
|
|
1604
|
-
});
|
|
1605
|
-
|
|
1606
|
-
test("Slack origin bumps default minKeepRecentUserTurns to 8", async () => {
|
|
1607
|
-
const provider = createProvider(() => ({
|
|
1608
|
-
content: [{ type: "text", text: "## Goals\n- slack thread context" }],
|
|
1609
|
-
model: "mock-model",
|
|
1610
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1611
|
-
stopReason: "end_turn",
|
|
1612
|
-
}));
|
|
1613
|
-
|
|
1614
|
-
// Use targetInputTokensOverride so the binary search is forced even
|
|
1615
|
-
// for a small history. Both managers see the same tight budget; the
|
|
1616
|
-
// only knob that varies is conversationOriginChannel.
|
|
1617
|
-
const config = makeConfig({ maxInputTokens: 12_000 });
|
|
1618
|
-
const long = "s".repeat(220);
|
|
1619
|
-
// 9 user turns: enough headroom for Slack's bumped floor of 8 to be
|
|
1620
|
-
// distinguishable from the default floor of 1.
|
|
1621
|
-
const history: Message[] = [];
|
|
1622
|
-
for (let i = 1; i <= 9; i++) {
|
|
1623
|
-
history.push(message("user", `u${i} ${long}`));
|
|
1624
|
-
history.push(message("assistant", `a${i} ${long}`));
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
const slackManager = new ContextWindowManager({
|
|
1628
|
-
provider,
|
|
1629
|
-
systemPrompt: "system prompt",
|
|
1630
|
-
config,
|
|
1631
|
-
});
|
|
1632
|
-
const slackResult = await slackManager.maybeCompact(history, undefined, {
|
|
1633
|
-
force: true,
|
|
1634
|
-
targetInputTokensOverride: 200,
|
|
1635
|
-
conversationOriginChannel: "slack",
|
|
1636
|
-
});
|
|
1637
|
-
|
|
1638
|
-
const defaultManager = new ContextWindowManager({
|
|
1639
|
-
provider,
|
|
1640
|
-
systemPrompt: "system prompt",
|
|
1641
|
-
config,
|
|
1642
|
-
});
|
|
1643
|
-
const defaultResult = await defaultManager.maybeCompact(
|
|
1644
|
-
history,
|
|
1645
|
-
undefined,
|
|
1646
|
-
{ force: true, targetInputTokensOverride: 200 },
|
|
1647
|
-
);
|
|
1648
|
-
|
|
1649
|
-
expect(slackResult.compacted).toBe(true);
|
|
1650
|
-
expect(defaultResult.compacted).toBe(true);
|
|
1651
|
-
// Default floor (1 user turn) compacts more of the history than the
|
|
1652
|
-
// Slack floor (8 user turns), which preserves more recent context.
|
|
1653
|
-
expect(defaultResult.compactedMessages).toBeGreaterThan(
|
|
1654
|
-
slackResult.compactedMessages,
|
|
1655
|
-
);
|
|
1656
|
-
// Slack keeps 8 of 9 user turns: 16 kept messages, 2 compacted.
|
|
1657
|
-
expect(slackResult.compactedMessages).toBe(2);
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
test("non-Slack origin keeps default minKeepRecentUserTurns of 1", async () => {
|
|
1661
|
-
const provider = createProvider(() => ({
|
|
1662
|
-
content: [{ type: "text", text: "## Goals\n- standard summary" }],
|
|
1663
|
-
model: "mock-model",
|
|
1664
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1665
|
-
stopReason: "end_turn",
|
|
1666
|
-
}));
|
|
1667
|
-
|
|
1668
|
-
const config = makeConfig({ maxInputTokens: 12_000 });
|
|
1669
|
-
const long = "n".repeat(220);
|
|
1670
|
-
const history: Message[] = [];
|
|
1671
|
-
for (let i = 1; i <= 9; i++) {
|
|
1672
|
-
history.push(message("user", `u${i} ${long}`));
|
|
1673
|
-
history.push(message("assistant", `a${i} ${long}`));
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// Telegram origin must behave identically to no-channel-hint default.
|
|
1677
|
-
const telegramManager = new ContextWindowManager({
|
|
1678
|
-
provider,
|
|
1679
|
-
systemPrompt: "system prompt",
|
|
1680
|
-
config,
|
|
1681
|
-
});
|
|
1682
|
-
const telegramResult = await telegramManager.maybeCompact(
|
|
1683
|
-
history,
|
|
1684
|
-
undefined,
|
|
1685
|
-
{
|
|
1686
|
-
force: true,
|
|
1687
|
-
targetInputTokensOverride: 200,
|
|
1688
|
-
conversationOriginChannel: "telegram",
|
|
1689
|
-
},
|
|
1690
|
-
);
|
|
1691
|
-
|
|
1692
|
-
const defaultManager = new ContextWindowManager({
|
|
1693
|
-
provider,
|
|
1694
|
-
systemPrompt: "system prompt",
|
|
1695
|
-
config,
|
|
1696
|
-
});
|
|
1697
|
-
const defaultResult = await defaultManager.maybeCompact(
|
|
1698
|
-
history,
|
|
1699
|
-
undefined,
|
|
1700
|
-
{ force: true, targetInputTokensOverride: 200 },
|
|
1701
|
-
);
|
|
1702
|
-
|
|
1703
|
-
expect(telegramResult.compacted).toBe(true);
|
|
1704
|
-
expect(defaultResult.compacted).toBe(true);
|
|
1705
|
-
expect(telegramResult.compactedMessages).toBe(
|
|
1706
|
-
defaultResult.compactedMessages,
|
|
1707
|
-
);
|
|
1708
|
-
});
|
|
1709
|
-
|
|
1710
|
-
test("explicit minKeepRecentUserTurns wins over Slack default", async () => {
|
|
1711
|
-
const provider = createProvider(() => ({
|
|
1712
|
-
content: [{ type: "text", text: "## Goals\n- emergency override" }],
|
|
1713
|
-
model: "mock-model",
|
|
1714
|
-
usage: { inputTokens: 60, outputTokens: 12 },
|
|
1715
|
-
stopReason: "end_turn",
|
|
1716
|
-
}));
|
|
1717
|
-
|
|
1718
|
-
const manager = new ContextWindowManager({
|
|
1719
|
-
provider,
|
|
1720
|
-
systemPrompt: "system prompt",
|
|
1721
|
-
config: makeConfig({
|
|
1722
|
-
maxInputTokens: 260,
|
|
1723
|
-
targetBudgetRatio: 0.28,
|
|
1724
|
-
}),
|
|
1725
|
-
});
|
|
1726
|
-
const long = "e".repeat(220);
|
|
1727
|
-
const history: Message[] = [
|
|
1728
|
-
message("user", `u1 ${long}`),
|
|
1729
|
-
message("assistant", `a1 ${long}`),
|
|
1730
|
-
message("user", `u2 ${long}`),
|
|
1731
|
-
];
|
|
1732
|
-
|
|
1733
|
-
// Emergency override (`minKeepRecentUserTurns: 0`) must take precedence
|
|
1734
|
-
// over the Slack-bumped default of 8 — this guards the agent loop's
|
|
1735
|
-
// context-too-large recovery path which always passes 0.
|
|
1736
|
-
const result = await manager.maybeCompact(history, undefined, {
|
|
1737
|
-
force: true,
|
|
1738
|
-
minKeepRecentUserTurns: 0,
|
|
1739
|
-
conversationOriginChannel: "slack",
|
|
1740
|
-
});
|
|
1741
|
-
expect(result.compacted).toBe(true);
|
|
1742
|
-
expect(result.compactedMessages).toBe(3);
|
|
1743
|
-
expect(result.messages).toHaveLength(1);
|
|
1744
|
-
});
|
|
1745
|
-
|
|
1746
|
-
test("summary provider call includes callSite: conversationSummarization", async () => {
|
|
1747
|
-
// Regression guard for JARVIS-587: without the callSite, the summary
|
|
1748
|
-
// call fell through to `llm.default` (opus + effort=max + thinking
|
|
1749
|
-
// enabled) and exceeded the 30s plugin pipeline budget on ~150k-token
|
|
1750
|
-
// transcripts. The fix is to route the summary call through the
|
|
1751
|
-
// dedicated `conversationSummarization` call-site config.
|
|
1752
|
-
const capturedOptions: (SendMessageOptions | undefined)[] = [];
|
|
1753
|
-
const provider: Provider = {
|
|
1754
|
-
name: "mock",
|
|
1755
|
-
async sendMessage(
|
|
1756
|
-
_messages: Message[],
|
|
1757
|
-
_tools: unknown,
|
|
1758
|
-
_systemPrompt: unknown,
|
|
1759
|
-
options?: SendMessageOptions,
|
|
1760
|
-
): Promise<ProviderResponse> {
|
|
1761
|
-
capturedOptions.push(options);
|
|
1762
|
-
return {
|
|
1763
|
-
content: [{ type: "text", text: "## Goals\n- summary" }],
|
|
1764
|
-
model: "mock-model",
|
|
1765
|
-
usage: { inputTokens: 50, outputTokens: 10 },
|
|
1766
|
-
stopReason: "end_turn",
|
|
1767
|
-
};
|
|
1768
|
-
},
|
|
1769
|
-
};
|
|
1770
|
-
const manager = new ContextWindowManager({
|
|
1771
|
-
provider,
|
|
1772
|
-
systemPrompt: "system prompt",
|
|
1773
|
-
config: makeConfig({ maxInputTokens: 600 }),
|
|
1774
|
-
});
|
|
1775
|
-
const long = "x".repeat(240);
|
|
1776
|
-
const history: Message[] = [
|
|
1777
|
-
message("user", `u1 ${long}`),
|
|
1778
|
-
message("assistant", `a1 ${long}`),
|
|
1779
|
-
message("user", `u2 ${long}`),
|
|
1780
|
-
message("assistant", `a2 ${long}`),
|
|
1781
|
-
message("user", `u3 ${long}`),
|
|
1782
|
-
message("assistant", `a3 ${long}`),
|
|
1783
|
-
];
|
|
1784
|
-
|
|
1785
|
-
const result = await manager.maybeCompact(history);
|
|
1786
|
-
expect(result.compacted).toBe(true);
|
|
1787
|
-
expect(capturedOptions.length).toBeGreaterThan(0);
|
|
1788
|
-
for (const options of capturedOptions) {
|
|
1789
|
-
expect(options?.config?.callSite).toBe("conversationSummarization");
|
|
1790
|
-
}
|
|
1791
|
-
});
|
|
1792
|
-
});
|
|
1793
|
-
|
|
1794
|
-
describe("stripCompactionOnlyInjections", () => {
|
|
1795
|
-
test("removes memory, turn_context, and workspace text blocks from user messages", () => {
|
|
1796
|
-
const messages: Message[] = [
|
|
1797
|
-
{
|
|
1798
|
-
role: "user",
|
|
1799
|
-
content: [
|
|
1800
|
-
{
|
|
1801
|
-
type: "text",
|
|
1802
|
-
text: "<memory __injected>\nrecall notes\n</memory>",
|
|
1803
|
-
},
|
|
1804
|
-
{
|
|
1805
|
-
type: "text",
|
|
1806
|
-
text: "<turn_context>\nActor: Alice\n</turn_context>",
|
|
1807
|
-
},
|
|
1808
|
-
{ type: "text", text: "real user content" },
|
|
1809
|
-
],
|
|
1810
|
-
},
|
|
1811
|
-
{
|
|
1812
|
-
role: "assistant",
|
|
1813
|
-
content: [{ type: "text", text: "assistant reply" }],
|
|
1814
|
-
},
|
|
1815
|
-
];
|
|
1816
|
-
const stripped = stripCompactionOnlyInjections(messages);
|
|
1817
|
-
expect(stripped).toHaveLength(2);
|
|
1818
|
-
const firstText = (stripped[0].content[0] as { text: string }).text;
|
|
1819
|
-
expect(firstText).toBe("real user content");
|
|
1820
|
-
expect(stripped[0].content).toHaveLength(1);
|
|
1821
|
-
});
|
|
1822
|
-
|
|
1823
|
-
test("drops user messages that become empty after stripping", () => {
|
|
1824
|
-
const messages: Message[] = [
|
|
1825
|
-
{
|
|
1826
|
-
role: "user",
|
|
1827
|
-
content: [
|
|
1828
|
-
{ type: "text", text: "<memory __injected>\nonly memory\n</memory>" },
|
|
1829
|
-
],
|
|
1830
|
-
},
|
|
1831
|
-
{ role: "user", content: [{ type: "text", text: "real content" }] },
|
|
1832
|
-
];
|
|
1833
|
-
const stripped = stripCompactionOnlyInjections(messages);
|
|
1834
|
-
expect(stripped).toHaveLength(1);
|
|
1835
|
-
expect((stripped[0].content[0] as { text: string }).text).toBe(
|
|
1836
|
-
"real content",
|
|
1837
|
-
);
|
|
1838
|
-
});
|
|
1839
|
-
|
|
1840
|
-
test("leaves assistant messages and non-text blocks untouched", () => {
|
|
1841
|
-
const messages: Message[] = [
|
|
1842
|
-
{
|
|
1843
|
-
role: "assistant",
|
|
1844
|
-
content: [
|
|
1845
|
-
{
|
|
1846
|
-
type: "text",
|
|
1847
|
-
text: "<turn_context>\nnot really injected\n</turn_context>",
|
|
1848
|
-
},
|
|
1849
|
-
],
|
|
1850
|
-
},
|
|
1851
|
-
{
|
|
1852
|
-
role: "user",
|
|
1853
|
-
content: [
|
|
1854
|
-
{
|
|
1855
|
-
type: "tool_result",
|
|
1856
|
-
tool_use_id: "t1",
|
|
1857
|
-
content: "<memory>fake</memory>",
|
|
1858
|
-
},
|
|
1859
|
-
{ type: "text", text: "user reply" },
|
|
1860
|
-
],
|
|
1861
|
-
},
|
|
1862
|
-
];
|
|
1863
|
-
const stripped = stripCompactionOnlyInjections(messages);
|
|
1864
|
-
expect(stripped).toHaveLength(2);
|
|
1865
|
-
expect((stripped[0].content[0] as { text: string }).text).toContain(
|
|
1866
|
-
"turn_context",
|
|
1867
|
-
);
|
|
1868
|
-
expect(stripped[1].content).toHaveLength(2);
|
|
1869
|
-
});
|
|
1870
|
-
|
|
1871
|
-
test("preserves user prose that merely mentions ambiguous tag names", () => {
|
|
1872
|
-
// Common-word bare tags embedded in legitimate user prose (discussions of
|
|
1873
|
-
// XML, system terminology, etc.) must survive stripping because they are
|
|
1874
|
-
// not shaped like a runtime injection — no leading newline after the
|
|
1875
|
-
// open tag, or other prose surrounds the tag.
|
|
1876
|
-
const messages: Message[] = [
|
|
1877
|
-
{
|
|
1878
|
-
role: "user",
|
|
1879
|
-
content: [
|
|
1880
|
-
{
|
|
1881
|
-
type: "text",
|
|
1882
|
-
text: "<memory> is a tag I'd like to add to my parser",
|
|
1883
|
-
},
|
|
1884
|
-
],
|
|
1885
|
-
},
|
|
1886
|
-
{
|
|
1887
|
-
role: "user",
|
|
1888
|
-
content: [
|
|
1889
|
-
{
|
|
1890
|
-
type: "text",
|
|
1891
|
-
text: "checking <workspace> usage across the repo, any thoughts?",
|
|
1892
|
-
},
|
|
1893
|
-
],
|
|
1894
|
-
},
|
|
1895
|
-
{
|
|
1896
|
-
role: "user",
|
|
1897
|
-
content: [
|
|
1898
|
-
{
|
|
1899
|
-
type: "text",
|
|
1900
|
-
text: "what is <knowledge_base> in this context?",
|
|
1901
|
-
},
|
|
1902
|
-
],
|
|
1903
|
-
},
|
|
1904
|
-
{
|
|
1905
|
-
role: "user",
|
|
1906
|
-
content: [
|
|
1907
|
-
{ type: "text", text: "<pkb> sounds like a short name — wrong?" },
|
|
1908
|
-
],
|
|
1909
|
-
},
|
|
1910
|
-
{
|
|
1911
|
-
role: "user",
|
|
1912
|
-
content: [
|
|
1913
|
-
{
|
|
1914
|
-
type: "text",
|
|
1915
|
-
text: "when the model hits a <system_reminder>, what happens next?",
|
|
1916
|
-
},
|
|
1917
|
-
],
|
|
1918
|
-
},
|
|
1919
|
-
];
|
|
1920
|
-
const stripped = stripCompactionOnlyInjections(messages);
|
|
1921
|
-
expect(stripped).toHaveLength(messages.length);
|
|
1922
|
-
for (let i = 0; i < messages.length; i++) {
|
|
1923
|
-
expect(stripped[i].content).toHaveLength(1);
|
|
1924
|
-
expect((stripped[i].content[0] as { text: string }).text).toBe(
|
|
1925
|
-
(messages[i].content[0] as { text: string }).text,
|
|
1926
|
-
);
|
|
1927
|
-
}
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
test("still strips runtime-shaped wrapped blocks for ambiguous tag names", () => {
|
|
1931
|
-
// Bare-tag blocks with a newline after the open tag and a matching close
|
|
1932
|
-
// tag (e.g. `<memory>\n...\n</memory>`) match the wrapped-strip path.
|
|
1933
|
-
// This covers both the current runtime emission shape and blocks
|
|
1934
|
-
// persisted before the `__injected` attribute existed — the prefix list
|
|
1935
|
-
// handles `__injected`-attributed tags, and the wrapped matcher handles
|
|
1936
|
-
// the bare-tag wrap shape.
|
|
1937
|
-
const messages: Message[] = [
|
|
1938
|
-
{
|
|
1939
|
-
role: "user",
|
|
1940
|
-
content: [
|
|
1941
|
-
{ type: "text", text: "<memory>\nlegacy recall blob\n</memory>" },
|
|
1942
|
-
{ type: "text", text: "actual user content" },
|
|
1943
|
-
],
|
|
1944
|
-
},
|
|
1945
|
-
{
|
|
1946
|
-
role: "user",
|
|
1947
|
-
content: [
|
|
1948
|
-
{
|
|
1949
|
-
type: "text",
|
|
1950
|
-
text: "<workspace>\nRoot: /home\nFiles: a, b\n</workspace>",
|
|
1951
|
-
},
|
|
1952
|
-
{ type: "text", text: "more prose" },
|
|
1953
|
-
],
|
|
1954
|
-
},
|
|
1955
|
-
{
|
|
1956
|
-
role: "user",
|
|
1957
|
-
content: [
|
|
1958
|
-
{
|
|
1959
|
-
type: "text",
|
|
1960
|
-
text: "<system_reminder>\nread your PKB\n</system_reminder>",
|
|
1961
|
-
},
|
|
1962
|
-
{ type: "text", text: "ok" },
|
|
1963
|
-
],
|
|
1964
|
-
},
|
|
1965
|
-
];
|
|
1966
|
-
const stripped = stripCompactionOnlyInjections(messages);
|
|
1967
|
-
expect(stripped).toHaveLength(3);
|
|
1968
|
-
for (const msg of stripped) {
|
|
1969
|
-
expect(msg.content).toHaveLength(1);
|
|
1970
|
-
}
|
|
1971
|
-
expect((stripped[0].content[0] as { text: string }).text).toBe(
|
|
1972
|
-
"actual user content",
|
|
1973
|
-
);
|
|
1974
|
-
expect((stripped[1].content[0] as { text: string }).text).toBe(
|
|
1975
|
-
"more prose",
|
|
1976
|
-
);
|
|
1977
|
-
expect((stripped[2].content[0] as { text: string }).text).toBe("ok");
|
|
1978
|
-
});
|
|
1979
|
-
|
|
1980
|
-
test("does not strip a user's inline snippet that is not shaped like an injection", () => {
|
|
1981
|
-
// A user quoting a `<memory>...</memory>` snippet alongside prose in the
|
|
1982
|
-
// SAME text block should survive — the block does not start with
|
|
1983
|
-
// `<memory>\n` (there's surrounding prose) so the wrapped-tag match
|
|
1984
|
-
// does not trigger.
|
|
1985
|
-
const messages: Message[] = [
|
|
1986
|
-
{
|
|
1987
|
-
role: "user",
|
|
1988
|
-
content: [
|
|
1989
|
-
{
|
|
1990
|
-
type: "text",
|
|
1991
|
-
text: "Here's the XML I'm working with: <memory>x</memory> — what do you think?",
|
|
1992
|
-
},
|
|
1993
|
-
],
|
|
1994
|
-
},
|
|
1995
|
-
];
|
|
1996
|
-
const stripped = stripCompactionOnlyInjections(messages);
|
|
1997
|
-
expect(stripped).toHaveLength(1);
|
|
1998
|
-
expect((stripped[0].content[0] as { text: string }).text).toContain(
|
|
1999
|
-
"<memory>x</memory>",
|
|
2000
|
-
);
|
|
2001
|
-
});
|
|
2002
|
-
});
|
|
2003
|
-
|
|
2004
|
-
describe("summarizer input excludes runtime injections", () => {
|
|
2005
|
-
test("maybeCompact does not pass memory/turn_context text to the summarizer", async () => {
|
|
2006
|
-
const seenPrompts: string[] = [];
|
|
2007
|
-
const provider = createProvider((messages) => {
|
|
2008
|
-
for (const msg of messages) {
|
|
2009
|
-
for (const block of msg.content) {
|
|
2010
|
-
if (block.type === "text") seenPrompts.push(block.text);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
return {
|
|
2014
|
-
content: [
|
|
2015
|
-
{
|
|
2016
|
-
type: "text",
|
|
2017
|
-
text: "## Facts Worth Remembering\n- summary produced",
|
|
2018
|
-
},
|
|
2019
|
-
],
|
|
2020
|
-
model: "mock",
|
|
2021
|
-
usage: { inputTokens: 100, outputTokens: 25 },
|
|
2022
|
-
stopReason: "end_turn",
|
|
2023
|
-
};
|
|
2024
|
-
});
|
|
2025
|
-
const manager = new ContextWindowManager({
|
|
2026
|
-
provider,
|
|
2027
|
-
systemPrompt: "system prompt",
|
|
2028
|
-
config: makeConfig({
|
|
2029
|
-
maxInputTokens: 2000,
|
|
2030
|
-
targetBudgetRatio: 0.4,
|
|
2031
|
-
compactThreshold: 0.35,
|
|
2032
|
-
}),
|
|
2033
|
-
});
|
|
2034
|
-
const long = "x".repeat(1500);
|
|
2035
|
-
const memoryBlob =
|
|
2036
|
-
"<memory __injected>\nBOB_ATTENDED_STANDUP_YESTERDAY\n</memory>";
|
|
2037
|
-
const turnCtx =
|
|
2038
|
-
"<turn_context>\nACTOR_METADATA_THAT_SHOULD_NOT_LEAK\n</turn_context>";
|
|
2039
|
-
const history: Message[] = [
|
|
2040
|
-
{
|
|
2041
|
-
role: "user",
|
|
2042
|
-
content: [
|
|
2043
|
-
{ type: "text", text: memoryBlob },
|
|
2044
|
-
{ type: "text", text: turnCtx },
|
|
2045
|
-
{ type: "text", text: `u1 ${long}` },
|
|
2046
|
-
],
|
|
2047
|
-
},
|
|
2048
|
-
message("assistant", `a1 ${long}`),
|
|
2049
|
-
message("user", `u2 ${long}`),
|
|
2050
|
-
message("assistant", `a2 ${long}`),
|
|
2051
|
-
message("user", `u3 ${long}`),
|
|
2052
|
-
];
|
|
2053
|
-
|
|
2054
|
-
const result = await manager.maybeCompact(history);
|
|
2055
|
-
expect(result.compacted).toBe(true);
|
|
2056
|
-
const joined = seenPrompts.join("\n");
|
|
2057
|
-
expect(joined).not.toContain("BOB_ATTENDED_STANDUP_YESTERDAY");
|
|
2058
|
-
expect(joined).not.toContain("ACTOR_METADATA_THAT_SHOULD_NOT_LEAK");
|
|
2059
|
-
expect(joined).not.toContain("<memory __injected>");
|
|
2060
|
-
expect(joined).not.toContain("<turn_context>");
|
|
2061
|
-
// Real conversation content should survive — at least one of the
|
|
2062
|
-
// middle turns (whose header/body is short enough to fit within the
|
|
2063
|
-
// capped transcript budget) should appear in the summarizer input.
|
|
2064
|
-
expect(joined).toMatch(/u2 |a1 /);
|
|
2065
|
-
});
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
|
-
describe("clampSummaryAtSectionBoundary", () => {
|
|
2069
|
-
test("returns the input unchanged when under the limit", () => {
|
|
2070
|
-
const summary = "## Decisions\nWe decided to ship.";
|
|
2071
|
-
expect(clampSummaryAtSectionBoundary(summary, 1000)).toBe(summary);
|
|
2072
|
-
});
|
|
2073
|
-
|
|
2074
|
-
test("truncates at a `## ` boundary when one exists in the allowed region", () => {
|
|
2075
|
-
const keeper = "## Facts\n" + "a".repeat(200);
|
|
2076
|
-
const dropped = "## Open Threads\n" + "b".repeat(500);
|
|
2077
|
-
const summary = `${keeper}\n${dropped}`;
|
|
2078
|
-
const maxChars = keeper.length + 20;
|
|
2079
|
-
const clamped = clampSummaryAtSectionBoundary(summary, maxChars);
|
|
2080
|
-
expect(clamped.endsWith("...")).toBe(true);
|
|
2081
|
-
expect(clamped).not.toContain("## Open Threads");
|
|
2082
|
-
expect(clamped).toContain("## Facts");
|
|
2083
|
-
// No mid-header cut: nothing that looks like a partial heading.
|
|
2084
|
-
expect(/##\s*$/.test(clamped)).toBe(false);
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
test("falls back to a hard cut when no section boundary is past the midpoint", () => {
|
|
2088
|
-
const body = "no section headers in this output " + "z".repeat(1000);
|
|
2089
|
-
const clamped = clampSummaryAtSectionBoundary(body, 100);
|
|
2090
|
-
expect(clamped.endsWith("...")).toBe(true);
|
|
2091
|
-
expect(clamped.length).toBeLessThanOrEqual(100);
|
|
2092
|
-
});
|
|
2093
|
-
});
|