@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,234 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Context window manager — the surface the rest of the daemon talks to
|
|
3
|
+
* when it needs to know whether and how to compact a conversation.
|
|
4
|
+
*
|
|
5
|
+
* The actual compaction work is delegated to {@link runAssistantDrivenCompaction}
|
|
6
|
+
* in `./compactor.js`, which hands the model the full conversation plus a
|
|
7
|
+
* user-role instruction message and lets the assistant write its own
|
|
8
|
+
* summary and choose its own cut point.
|
|
9
|
+
*
|
|
10
|
+
* This module retains a small set of legacy exports — `CONTEXT_SUMMARY_MARKER`,
|
|
11
|
+
* `createContextSummaryMessage`, `getSummaryFromContextMessage` — because
|
|
12
|
+
* conversation reload, fork inheritance, and Slack chronological-context
|
|
13
|
+
* assembly all detect a previously-produced summary via the marker. The
|
|
14
|
+
* marker is wrapped around the assistant-role memory message we emit on
|
|
15
|
+
* successful compaction so those code paths keep working unchanged.
|
|
16
|
+
*/
|
|
17
|
+
import { getConfig } from "../config/loader.js";
|
|
18
|
+
import type { CompactionConfig } from "../config/schemas/compaction.js";
|
|
4
19
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
5
20
|
import type { ContextWindowConfig } from "../config/types.js";
|
|
6
21
|
import type {
|
|
7
22
|
ContentBlock,
|
|
8
|
-
ImageContent,
|
|
9
23
|
Message,
|
|
10
24
|
Provider,
|
|
25
|
+
ToolDefinition,
|
|
11
26
|
} from "../providers/types.js";
|
|
12
|
-
import { resolveBundledDir } from "../util/bundled-asset.js";
|
|
13
27
|
import { getLogger } from "../util/logger.js";
|
|
14
|
-
import { safeStringSlice } from "../util/unicode.js";
|
|
15
28
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./token-estimator.js";
|
|
20
|
-
import { truncateToolResultsAcrossHistory } from "./tool-result-truncation.js";
|
|
29
|
+
type CompactionRunArgs,
|
|
30
|
+
runAssistantDrivenCompaction,
|
|
31
|
+
} from "./compactor.js";
|
|
32
|
+
import { estimatePromptTokens } from "./token-estimator.js";
|
|
21
33
|
|
|
22
34
|
const log = getLogger("context-window");
|
|
23
35
|
|
|
24
36
|
export const CONTEXT_SUMMARY_MARKER = "<context_summary>";
|
|
25
|
-
const
|
|
26
|
-
const MAX_BLOCK_PREVIEW_CHARS = 3000;
|
|
27
|
-
const MAX_FALLBACK_SUMMARY_CHARS = 12000;
|
|
28
|
-
const COMPACTION_COOLDOWN_MS = 2 * 60 * 1000;
|
|
29
|
-
const MIN_GAIN_TOKENS_DURING_COOLDOWN = 1200;
|
|
30
|
-
const SEVERE_PRESSURE_RATIO = 0.95;
|
|
31
|
-
const COMPACTION_TOOL_RESULT_MAX_CHARS = 6_000;
|
|
32
|
-
const MIN_COMPACTABLE_PERSISTED_MESSAGES = 2;
|
|
37
|
+
const CONTEXT_SUMMARY_CLOSE = "</context_summary>";
|
|
33
38
|
const INTERNAL_CONTEXT_SUMMARY_MESSAGES = new WeakSet<Message>();
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*/
|
|
40
|
-
const SUMMARY_COMPRESSION_PRESSURE_RATIO = 0.6;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Text-block prefixes that persist in live history (for prefix-caching
|
|
44
|
-
* stability and model grounding) but pollute the summarizer's view of the
|
|
45
|
-
* actual conversation. These blocks are system-metadata attached to user
|
|
46
|
-
* turns — memory injections, turn context, workspace hints, etc. They are
|
|
47
|
-
* stripped ONLY from the messages fed to the summarization LLM call. Live
|
|
48
|
-
* history is never mutated, so prefix caching is preserved.
|
|
49
|
-
*
|
|
50
|
-
* This list intentionally overlaps with `RUNTIME_INJECTION_PREFIXES` in
|
|
51
|
-
* `conversation-runtime-assembly.ts`. That list governs in-flight turn
|
|
52
|
-
* assembly via pure prefix matching; this one governs compaction input.
|
|
53
|
-
* Keep the two lists in sync when a new injection type is added.
|
|
54
|
-
*
|
|
55
|
-
* Compaction strip coverage is two-tier: this prefix list catches
|
|
56
|
-
* internal-vocabulary tags and any tag carrying the `__injected`
|
|
57
|
-
* attribute, while `COMPACTION_ONLY_WRAPPED_STRIP_TAGS` below matches
|
|
58
|
-
* ambiguous bare-tag blocks that are shaped like a runtime-emitted
|
|
59
|
-
* open/close wrap. A new ambiguous tag added upstream needs to be
|
|
60
|
-
* evaluated against both tiers — internal-vocabulary names go here,
|
|
61
|
-
* and names whose bare form collides with ordinary English
|
|
62
|
-
* (`<memory>`, `<workspace>`, `<knowledge_base>`, `<pkb>`,
|
|
63
|
-
* `<system_reminder>`) go in the wrapped-strip list so user prose
|
|
64
|
-
* mentioning the tag is preserved.
|
|
65
|
-
*/
|
|
66
|
-
const COMPACTION_ONLY_STRIP_PREFIXES = [
|
|
67
|
-
"<memory __injected>",
|
|
68
|
-
"<memory_image __injected>",
|
|
69
|
-
"</memory_image>",
|
|
70
|
-
"<memory_context __injected>",
|
|
71
|
-
"<turn_context>",
|
|
72
|
-
"<channel_turn_context>",
|
|
73
|
-
"<guardian_context>",
|
|
74
|
-
"<inbound_actor_context>",
|
|
75
|
-
"<interface_turn_context>",
|
|
76
|
-
"<workspace_top_level>",
|
|
77
|
-
"<now_scratchpad>",
|
|
78
|
-
"<NOW.md Always keep this up to date",
|
|
79
|
-
"<active_thread>",
|
|
80
|
-
"<active_subagents>",
|
|
81
|
-
"<active_workspace>",
|
|
82
|
-
"<active_dynamic_page>",
|
|
83
|
-
"<channel_capabilities>",
|
|
84
|
-
"<channel_command_context>",
|
|
85
|
-
"<voice_call_control>",
|
|
86
|
-
"<transport_hints>",
|
|
87
|
-
"<system_notice>",
|
|
88
|
-
"<non_interactive_context>",
|
|
89
|
-
"<temporal_context>",
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Tags whose bare form (`<tag>`) is common English vocabulary or markup a
|
|
94
|
-
* user might legitimately type in prose. For these we only strip a text
|
|
95
|
-
* block if it is shaped exactly like a runtime injection: starts with
|
|
96
|
-
* `<tag>\n` and ends with `</tag>`. This bare-tag wrapped shape
|
|
97
|
-
* (e.g. `<memory>\n...\n</memory>`) appears in persisted history
|
|
98
|
-
* alongside the `__injected`-attributed variants, which the prefix list
|
|
99
|
-
* above already catches via `<memory __injected>`. A user who mentions
|
|
100
|
-
* `<memory>` in a sentence or inlines `<workspace>...</workspace>` within
|
|
101
|
-
* other prose will not match this shape.
|
|
102
|
-
*/
|
|
103
|
-
const COMPACTION_ONLY_WRAPPED_STRIP_TAGS = [
|
|
104
|
-
"memory",
|
|
105
|
-
"memory_context",
|
|
106
|
-
"workspace",
|
|
107
|
-
"knowledge_base",
|
|
108
|
-
"pkb",
|
|
109
|
-
"system_reminder",
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
function isCompactionInjectedBlock(text: string): boolean {
|
|
113
|
-
if (COMPACTION_ONLY_STRIP_PREFIXES.some((p) => text.startsWith(p))) {
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
return COMPACTION_ONLY_WRAPPED_STRIP_TAGS.some(
|
|
117
|
-
(tag) => text.startsWith(`<${tag}>\n`) && text.endsWith(`</${tag}>`),
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Remove text blocks that look like runtime injections from user messages.
|
|
123
|
-
* Non-text blocks (images, tool_use, tool_result, etc.) are untouched.
|
|
124
|
-
* Empty messages (every block filtered out) are dropped from the output.
|
|
125
|
-
*
|
|
126
|
-
* Used only on the `compactableMessages` slice right before it is
|
|
127
|
-
* serialized for the summarization LLM — the caller's original message
|
|
128
|
-
* array is never mutated.
|
|
129
|
-
*/
|
|
130
|
-
export function stripCompactionOnlyInjections(messages: Message[]): Message[] {
|
|
131
|
-
return messages
|
|
132
|
-
.map((message) => {
|
|
133
|
-
if (message.role !== "user") return message;
|
|
134
|
-
const nextContent = message.content.filter((block) => {
|
|
135
|
-
if (block.type !== "text") return true;
|
|
136
|
-
return !isCompactionInjectedBlock(block.text);
|
|
137
|
-
});
|
|
138
|
-
if (nextContent.length === message.content.length) return message;
|
|
139
|
-
if (nextContent.length === 0) return null;
|
|
140
|
-
return { ...message, content: nextContent };
|
|
141
|
-
})
|
|
142
|
-
.filter(
|
|
143
|
-
(message): message is NonNullable<typeof message> => message != null,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Load the compaction summary system prompt from the bundled markdown asset.
|
|
149
|
-
*
|
|
150
|
-
* `resolveBundledDir` handles the compiled-binary case where the caller path
|
|
151
|
-
* points to `/$bunfs/` and the asset lives next to the executable (macOS app
|
|
152
|
-
* bundle `Contents/Resources/` or sibling dir). In source mode it falls back
|
|
153
|
-
* to the sibling `prompts/` directory.
|
|
154
|
-
*/
|
|
155
|
-
export function loadCompactPrompt(): string {
|
|
156
|
-
const callerDir = import.meta.dirname ?? __dirname;
|
|
157
|
-
const promptsDir = resolveBundledDir(callerDir, "prompts", "compact-prompts");
|
|
158
|
-
const promptPath = join(promptsDir, "compact.md");
|
|
159
|
-
const contents = readFileSync(promptPath, "utf-8");
|
|
160
|
-
if (contents.length === 0) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`compact.md at ${promptPath} is empty — compaction summary prompt missing`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
return contents;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Hardcoded fallback prompt used when the bundled `compact.md` asset is
|
|
170
|
-
* missing or unreadable, so the daemon can still compact conversations
|
|
171
|
-
* rather than failing module import at startup.
|
|
172
|
-
*/
|
|
173
|
-
const SUMMARY_PROMPT_FALLBACK = [
|
|
174
|
-
"You are summarizing a long conversation so that the assistant can keep working with it after older messages are dropped. Your summary will REPLACE those messages — the assistant's only access to what was said earlier will be what you write here.",
|
|
175
|
-
"",
|
|
176
|
-
"Be thorough. Capture what happened, why it mattered, what's unresolved, and what was felt. Do not compress away emotional tone, relationship context, or nuance. Keep specific details (names, numbers, file paths, commands, URLs, exact phrasings) when they might matter later.",
|
|
177
|
-
"",
|
|
178
|
-
"Target length: aim for 1500–4000 tokens. Use the upper end when the conversation is rich in decisions, relationships, emotional content, or threads that are still open. Use the lower end for short or simple task execution.",
|
|
179
|
-
"",
|
|
180
|
-
"Open with a 1–2 paragraph narrative describing what the conversation is about and where it currently stands. Then use `## ` section headers. Use these when they apply; skip sections that have nothing to say; add your own headers when something doesn't fit:",
|
|
181
|
-
"- `## What We're Working On`",
|
|
182
|
-
"- `## Decisions & Commitments`",
|
|
183
|
-
"- `## Facts Worth Remembering`",
|
|
184
|
-
"- `## Open Threads`",
|
|
185
|
-
"- `## Emotional Arc / Relationship Notes` (include when relevant)",
|
|
186
|
-
"- `## Artifacts & References`",
|
|
187
|
-
"",
|
|
188
|
-
"If an existing summary is provided, update it: merge new information in, prefer the most recent and explicit detail on conflicts, and preserve anything still unresolved or still true. Do not restart from scratch.",
|
|
189
|
-
"",
|
|
190
|
-
"Never include in the summary: content inside `<memory __injected>`, `<memory>`, `<turn_context>`, `<workspace>`, `<knowledge_base>`, `<system_reminder>`, `<now_scratchpad>`, `<NOW.md …>`, `<active_thread>`, `<channel_capabilities>`, `<transport_hints>`, `<system_notice>`, or any other angle-bracket-tagged system blocks. Tool-call boilerplate (retries, failed attempts the assistant recovered from, routine status updates) — summarize the outcome instead. Repetitive chit-chat that adds nothing.",
|
|
191
|
-
"",
|
|
192
|
-
'Thread anchors (Slack only): if the input includes a "Retained Thread References" section, each listed reply cites its parent via `→ Mxxxxxx`. If that parent appears in the Transcript, preserve its text verbatim. Omit when absent.',
|
|
193
|
-
"",
|
|
194
|
-
"Return only the summary itself in markdown — no preamble, no meta-commentary.",
|
|
195
|
-
].join("\n");
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Load the compact prompt with graceful fallback. If `loader` throws (missing
|
|
199
|
-
* or unreadable bundled asset, partial deployment, filesystem corruption),
|
|
200
|
-
* logs a warning and returns the hardcoded fallback string so module import
|
|
201
|
-
* never fails. The loader is injectable for testability.
|
|
202
|
-
*/
|
|
203
|
-
export function loadCompactPromptOrFallback(
|
|
204
|
-
loader: () => string = loadCompactPrompt,
|
|
205
|
-
): string {
|
|
206
|
-
try {
|
|
207
|
-
return loader();
|
|
208
|
-
} catch (err) {
|
|
209
|
-
log.warn(
|
|
210
|
-
{ err },
|
|
211
|
-
"Failed to load compact.md from bundle; using inline fallback prompt. The bundled asset may be missing or unreadable.",
|
|
212
|
-
);
|
|
213
|
-
return SUMMARY_PROMPT_FALLBACK;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const SUMMARY_SYSTEM_PROMPT = loadCompactPromptOrFallback();
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Pattern matching a Slack-style reply tag-line's parent-alias reference.
|
|
221
|
-
* The chronological renderer emits reply lines as
|
|
222
|
-
* `[MM/DD/YY HH:MM @sender → Mxxxxxx]: body`, or, for edited replies,
|
|
223
|
-
* `[MM/DD/YY HH:MM @sender → Mxxxxxx, edited MM/DD/YY HH:MM]: body`. The
|
|
224
|
-
* character after the 6-hex parent alias is therefore `]` for a plain reply
|
|
225
|
-
* or `,` for an edited one — the regex accepts either. `Mxxxxxx` is the
|
|
226
|
-
* first 6 hex chars of sha256(threadTs). A retained-tail text block that
|
|
227
|
-
* contains this pattern is carrying a live reference to a parent that may
|
|
228
|
-
* still live in the compactable region — the summarizer needs to know about
|
|
229
|
-
* it to act on the Thread-anchors clause of SUMMARY_SYSTEM_PROMPT.
|
|
230
|
-
*/
|
|
231
|
-
const THREAD_REPLY_REFERENCE_PATTERN = /→ M[0-9a-f]{6}[,\]]/;
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Public types — preserved for downstream consumers (agent loop, conversation,
|
|
42
|
+
// plugin pipeline, applyCompactionResult, routes/playground/force-compact).
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
232
44
|
|
|
233
45
|
export interface ContextWindowResult {
|
|
234
46
|
messages: Message[];
|
|
@@ -250,12 +62,6 @@ export interface ContextWindowResult {
|
|
|
250
62
|
summaryRawResponses?: unknown[];
|
|
251
63
|
summaryText: string;
|
|
252
64
|
reason?: string;
|
|
253
|
-
/**
|
|
254
|
-
* True when the summary LLM call threw and the local fallback produced the
|
|
255
|
-
* summary. Callers use this to distinguish provider-side summary failures
|
|
256
|
-
* from successful compactions so they can apply circuit-breaker logic
|
|
257
|
-
* without losing the fallback-compacted messages.
|
|
258
|
-
*/
|
|
259
65
|
summaryFailed?: boolean;
|
|
260
66
|
}
|
|
261
67
|
|
|
@@ -266,41 +72,27 @@ export interface ShouldCompactResult {
|
|
|
266
72
|
|
|
267
73
|
export interface ContextWindowCompactOptions {
|
|
268
74
|
lastCompactedAt?: number;
|
|
269
|
-
/**
|
|
75
|
+
/** Skip the auto-threshold check (used for /compact and recovery). */
|
|
270
76
|
force?: boolean;
|
|
271
77
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* (except the summary message itself). When omitted, the default floor
|
|
275
|
-
* is `1` (or `8` when `conversationOriginChannel === "slack"`).
|
|
276
|
-
*/
|
|
277
|
-
minKeepRecentUserTurns?: number;
|
|
278
|
-
/**
|
|
279
|
-
* Origin channel hint used when `minKeepRecentUserTurns` is omitted.
|
|
280
|
-
* Slack-originated conversations bump the default keep floor so multi-turn
|
|
281
|
-
* thread context (replies, quoted messages) is not summarized away too
|
|
282
|
-
* aggressively. Explicit `minKeepRecentUserTurns` overrides this hint.
|
|
283
|
-
*/
|
|
284
|
-
conversationOriginChannel?: string;
|
|
285
|
-
/**
|
|
286
|
-
* Per-conversation inference-profile override forwarded to the summary LLM
|
|
287
|
-
* call and usage attribution.
|
|
78
|
+
* Per-conversation inference-profile override forwarded to the compaction
|
|
79
|
+
* LLM call.
|
|
288
80
|
*/
|
|
289
81
|
overrideProfile?: string | null;
|
|
290
82
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* — i.e. the override may only demand a *stricter* fit. Passing a looser
|
|
294
|
-
* value has no effect. Intended for forced recovery paths that need a
|
|
295
|
-
* tighter target than the default.
|
|
83
|
+
* Pre-computed token estimate from a prior {@link shouldCompact} call.
|
|
84
|
+
* Avoids a redundant tokenization pass when the caller already has one.
|
|
296
85
|
*/
|
|
297
|
-
|
|
86
|
+
precomputedEstimate?: number;
|
|
298
87
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
88
|
+
* Legacy fields retained for backwards compatibility with existing
|
|
89
|
+
* callers. The new assistant-driven compactor does not consume them —
|
|
90
|
+
* the model decides where to cut and what to keep — but accepting them
|
|
91
|
+
* here lets callers keep their existing call sites unchanged.
|
|
302
92
|
*/
|
|
303
|
-
|
|
93
|
+
minKeepRecentUserTurns?: number;
|
|
94
|
+
conversationOriginChannel?: string;
|
|
95
|
+
targetInputTokensOverride?: number;
|
|
304
96
|
}
|
|
305
97
|
|
|
306
98
|
export interface ContextWindowManagerOptions {
|
|
@@ -309,36 +101,83 @@ export interface ContextWindowManagerOptions {
|
|
|
309
101
|
config: ContextWindowConfig;
|
|
310
102
|
/** Pre-computed tool token budget to include in all estimations. */
|
|
311
103
|
toolTokenBudget?: number;
|
|
104
|
+
/** Conversation ID — required for image-manifest and timestamp lookups. */
|
|
105
|
+
conversationId?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Optional tools resolver. The compactor passes tools to the provider on
|
|
108
|
+
* the compaction call so the cached prefix (system prompt + tools +
|
|
109
|
+
* conversation messages) matches the agent's main-turn cache key.
|
|
110
|
+
*/
|
|
111
|
+
resolveTools?: () => ToolDefinition[] | undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Summary-message helpers (used by lifecycle rehydrate + fork inheritance)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the synthetic memory message that heads a compacted conversation.
|
|
120
|
+
* Produces an `assistant`-role message wrapped in `<context_summary>` tags
|
|
121
|
+
* so reload and inheritance paths can recognize it via
|
|
122
|
+
* {@link getSummaryFromContextMessage}.
|
|
123
|
+
*/
|
|
124
|
+
export function createContextSummaryMessage(summary: string): Message {
|
|
125
|
+
const message: Message = {
|
|
126
|
+
role: "assistant",
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `${CONTEXT_SUMMARY_MARKER}\n${summary}\n${CONTEXT_SUMMARY_CLOSE}`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
INTERNAL_CONTEXT_SUMMARY_MESSAGES.add(message);
|
|
135
|
+
return message;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getSummaryFromContextMessage(
|
|
139
|
+
message: Message | undefined,
|
|
140
|
+
): string | null {
|
|
141
|
+
if (!message) return null;
|
|
142
|
+
const text = extractText(message.content).trim();
|
|
143
|
+
if (!text.startsWith(CONTEXT_SUMMARY_MARKER)) return null;
|
|
144
|
+
if (!INTERNAL_CONTEXT_SUMMARY_MESSAGES.has(message)) return null;
|
|
145
|
+
let inner = text.slice(CONTEXT_SUMMARY_MARKER.length);
|
|
146
|
+
const closeIdx = inner.lastIndexOf(CONTEXT_SUMMARY_CLOSE);
|
|
147
|
+
if (closeIdx !== -1) inner = inner.slice(0, closeIdx);
|
|
148
|
+
return inner.trim();
|
|
312
149
|
}
|
|
313
150
|
|
|
151
|
+
function extractText(content: ContentBlock[]): string {
|
|
152
|
+
return content
|
|
153
|
+
.filter(
|
|
154
|
+
(b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
|
|
155
|
+
)
|
|
156
|
+
.map((b) => b.text)
|
|
157
|
+
.join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// ContextWindowManager
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
314
164
|
export class ContextWindowManager {
|
|
315
165
|
private readonly provider: Provider;
|
|
316
166
|
private readonly _systemPrompt: string | (() => string);
|
|
317
167
|
private config: ContextWindowConfig;
|
|
318
168
|
private readonly toolTokenBudget: number;
|
|
169
|
+
private readonly conversationId: string | undefined;
|
|
170
|
+
private readonly resolveTools:
|
|
171
|
+
| (() => ToolDefinition[] | undefined)
|
|
172
|
+
| undefined;
|
|
319
173
|
/**
|
|
320
174
|
* Number of leading messages that are non-persisted (injected inherited
|
|
321
|
-
* context from a parent conversation).
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* after a successful compaction pass.
|
|
175
|
+
* context from a parent conversation). The compactor subtracts this from
|
|
176
|
+
* `compactedMessages` so `compactedPersistedMessages` only reflects DB
|
|
177
|
+
* rows. Decremented after a successful compaction.
|
|
325
178
|
*/
|
|
326
179
|
nonPersistedPrefixCount = 0;
|
|
327
|
-
/**
|
|
328
|
-
* True when the message at index 0 is a context summary that was inherited
|
|
329
|
-
* from a parent fork (i.e. injected as part of the non-persisted prefix),
|
|
330
|
-
* rather than produced by this conversation's own compaction. The parent
|
|
331
|
-
* summary sits at index 0 but is excluded from `compactableMessages` by
|
|
332
|
-
* `summaryOffset`, so its slot in `nonPersistedPrefixCount` must be
|
|
333
|
-
* accounted for separately. Cleared after the first compaction replaces
|
|
334
|
-
* the parent summary with a child-owned one.
|
|
335
|
-
*/
|
|
336
180
|
summaryIsInjected = false;
|
|
337
|
-
/**
|
|
338
|
-
* Cached resolved system prompt. Lazily populated on first access via the
|
|
339
|
-
* `systemPrompt` getter and cleared after each compaction pass so the next
|
|
340
|
-
* pass picks up any prompt changes.
|
|
341
|
-
*/
|
|
342
181
|
private _resolvedSystemPrompt: string | undefined;
|
|
343
182
|
|
|
344
183
|
constructor(options: ContextWindowManagerOptions) {
|
|
@@ -346,27 +185,21 @@ export class ContextWindowManager {
|
|
|
346
185
|
this._systemPrompt = options.systemPrompt;
|
|
347
186
|
this.config = options.config;
|
|
348
187
|
this.toolTokenBudget = options.toolTokenBudget ?? 0;
|
|
188
|
+
this.conversationId = options.conversationId;
|
|
189
|
+
this.resolveTools = options.resolveTools;
|
|
349
190
|
}
|
|
350
191
|
|
|
351
192
|
updateConfig(config: ContextWindowConfig): void {
|
|
352
193
|
this.config = config;
|
|
353
194
|
}
|
|
354
195
|
|
|
355
|
-
/**
|
|
356
|
-
* Provider key for the local token estimator. Wrapper providers (e.g.
|
|
357
|
-
* OpenRouter routing to `anthropic/*`) override `tokenEstimationProvider`
|
|
358
|
-
* so image/PDF sizing uses the same rules as the upstream API instead of
|
|
359
|
-
* the generic `base64/4` fallback.
|
|
360
|
-
*/
|
|
361
196
|
private get estimationProviderName(): string {
|
|
362
197
|
return this.provider.tokenEstimationProvider ?? this.provider.name;
|
|
363
198
|
}
|
|
364
199
|
|
|
365
|
-
/** Lazily resolve and cache the system prompt for the duration of a compaction pass. */
|
|
366
200
|
private get systemPrompt(): string {
|
|
367
|
-
if (this._resolvedSystemPrompt !== undefined)
|
|
201
|
+
if (this._resolvedSystemPrompt !== undefined)
|
|
368
202
|
return this._resolvedSystemPrompt;
|
|
369
|
-
}
|
|
370
203
|
const resolved =
|
|
371
204
|
typeof this._systemPrompt === "function"
|
|
372
205
|
? this._systemPrompt()
|
|
@@ -379,21 +212,26 @@ export class ContextWindowManager {
|
|
|
379
212
|
this._resolvedSystemPrompt = undefined;
|
|
380
213
|
}
|
|
381
214
|
|
|
215
|
+
private resolveCompactionConfig(): CompactionConfig {
|
|
216
|
+
return getConfig().compaction;
|
|
217
|
+
}
|
|
218
|
+
|
|
382
219
|
/**
|
|
383
|
-
* Cheap pre-check
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
220
|
+
* Cheap pre-check — estimate the current token count and compare against
|
|
221
|
+
* `compaction.autoThreshold`. Callers pass the estimate back through
|
|
222
|
+
* `precomputedEstimate` on the {@link maybeCompact} call to avoid
|
|
223
|
+
* re-tokenizing the same history twice.
|
|
387
224
|
*/
|
|
388
225
|
shouldCompact(messages: Message[]): ShouldCompactResult {
|
|
389
|
-
|
|
226
|
+
const compaction = this.resolveCompactionConfig();
|
|
227
|
+
if (!compaction.enabled) return { needed: false, estimatedTokens: 0 };
|
|
390
228
|
try {
|
|
391
229
|
const estimated = estimatePromptTokens(messages, this.systemPrompt, {
|
|
392
230
|
providerName: this.estimationProviderName,
|
|
393
231
|
toolTokenBudget: this.toolTokenBudget,
|
|
394
232
|
});
|
|
395
233
|
const threshold = Math.floor(
|
|
396
|
-
this.config.maxInputTokens *
|
|
234
|
+
this.config.maxInputTokens * compaction.autoThreshold,
|
|
397
235
|
);
|
|
398
236
|
return { needed: estimated >= threshold, estimatedTokens: estimated };
|
|
399
237
|
} finally {
|
|
@@ -418,6 +256,7 @@ export class ContextWindowManager {
|
|
|
418
256
|
signal?: AbortSignal,
|
|
419
257
|
options?: ContextWindowCompactOptions,
|
|
420
258
|
): Promise<ContextWindowResult> {
|
|
259
|
+
const compaction = this.resolveCompactionConfig();
|
|
421
260
|
const previousEstimatedInputTokens =
|
|
422
261
|
options?.precomputedEstimate ??
|
|
423
262
|
estimatePromptTokens(messages, this.systemPrompt, {
|
|
@@ -425,1091 +264,115 @@ export class ContextWindowManager {
|
|
|
425
264
|
toolTokenBudget: this.toolTokenBudget,
|
|
426
265
|
});
|
|
427
266
|
const thresholdTokens = Math.floor(
|
|
428
|
-
this.config.maxInputTokens *
|
|
267
|
+
this.config.maxInputTokens * compaction.autoThreshold,
|
|
429
268
|
);
|
|
430
|
-
const existingSummary = getSummaryFromContextMessage(messages[0]);
|
|
431
269
|
|
|
432
|
-
if (!
|
|
433
|
-
return {
|
|
434
|
-
messages,
|
|
435
|
-
compacted: false,
|
|
436
|
-
previousEstimatedInputTokens,
|
|
437
|
-
estimatedInputTokens: previousEstimatedInputTokens,
|
|
270
|
+
if (!compaction.enabled) {
|
|
271
|
+
return noopResult(messages, previousEstimatedInputTokens, {
|
|
438
272
|
maxInputTokens: this.config.maxInputTokens,
|
|
439
273
|
thresholdTokens,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
summaryCalls: 0,
|
|
443
|
-
summaryInputTokens: 0,
|
|
444
|
-
summaryOutputTokens: 0,
|
|
445
|
-
summaryModel: "",
|
|
446
|
-
summaryText: existingSummary ?? "",
|
|
447
|
-
reason: "context window compaction disabled",
|
|
448
|
-
};
|
|
274
|
+
reason: "compaction disabled",
|
|
275
|
+
});
|
|
449
276
|
}
|
|
450
277
|
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
278
|
+
if (this.conversationId == null) {
|
|
279
|
+
// The compactor needs the conversation id to look up image
|
|
280
|
+
// attachments and DB timestamps. If we don't have one (legacy test
|
|
281
|
+
// path, ad-hoc instantiation), skip — never fabricate one.
|
|
282
|
+
log.warn(
|
|
283
|
+
"ContextWindowManager has no conversationId — skipping compaction",
|
|
284
|
+
);
|
|
285
|
+
return noopResult(messages, previousEstimatedInputTokens, {
|
|
457
286
|
maxInputTokens: this.config.maxInputTokens,
|
|
458
287
|
thresholdTokens,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
summaryCalls: 0,
|
|
462
|
-
summaryInputTokens: 0,
|
|
463
|
-
summaryOutputTokens: 0,
|
|
464
|
-
summaryModel: "",
|
|
465
|
-
summaryText: existingSummary ?? "",
|
|
466
|
-
reason: "below compaction threshold",
|
|
467
|
-
};
|
|
288
|
+
reason: "no conversation id",
|
|
289
|
+
});
|
|
468
290
|
}
|
|
469
291
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (userTurnStarts.length === 0) {
|
|
473
|
-
return {
|
|
474
|
-
messages,
|
|
475
|
-
compacted: false,
|
|
476
|
-
previousEstimatedInputTokens,
|
|
477
|
-
estimatedInputTokens: previousEstimatedInputTokens,
|
|
292
|
+
if (!options?.force && previousEstimatedInputTokens < thresholdTokens) {
|
|
293
|
+
return noopResult(messages, previousEstimatedInputTokens, {
|
|
478
294
|
maxInputTokens: this.config.maxInputTokens,
|
|
479
295
|
thresholdTokens,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
summaryCalls: 0,
|
|
483
|
-
summaryInputTokens: 0,
|
|
484
|
-
summaryOutputTokens: 0,
|
|
485
|
-
summaryModel: "",
|
|
486
|
-
summaryText: existingSummary ?? "",
|
|
487
|
-
reason: "no user turns available for compaction",
|
|
488
|
-
};
|
|
296
|
+
reason: "below auto threshold",
|
|
297
|
+
});
|
|
489
298
|
}
|
|
490
299
|
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
300
|
+
const args: CompactionRunArgs = {
|
|
301
|
+
conversationId: this.conversationId,
|
|
302
|
+
messages,
|
|
303
|
+
provider: this.provider,
|
|
304
|
+
systemPrompt: this.systemPrompt,
|
|
305
|
+
tools: this.resolveTools?.(),
|
|
306
|
+
compaction,
|
|
307
|
+
maxInputTokens: this.config.maxInputTokens,
|
|
496
308
|
previousEstimatedInputTokens,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const { messages: truncatedMessages, truncatedCount } =
|
|
503
|
-
truncateToolResultsAcrossHistory(
|
|
504
|
-
messages,
|
|
505
|
-
COMPACTION_TOOL_RESULT_MAX_CHARS,
|
|
506
|
-
);
|
|
507
|
-
const didTruncate = truncatedCount > 0;
|
|
508
|
-
const estimatedAfterTruncation = didTruncate
|
|
509
|
-
? estimatePromptTokens(truncatedMessages, this.systemPrompt, {
|
|
510
|
-
providerName: this.estimationProviderName,
|
|
511
|
-
toolTokenBudget: this.toolTokenBudget,
|
|
512
|
-
})
|
|
513
|
-
: previousEstimatedInputTokens;
|
|
514
|
-
return {
|
|
515
|
-
messages: truncatedMessages,
|
|
516
|
-
compacted: didTruncate,
|
|
517
|
-
previousEstimatedInputTokens,
|
|
518
|
-
estimatedInputTokens: estimatedAfterTruncation,
|
|
519
|
-
maxInputTokens: this.config.maxInputTokens,
|
|
520
|
-
thresholdTokens,
|
|
521
|
-
compactedMessages: 0,
|
|
522
|
-
compactedPersistedMessages: 0,
|
|
523
|
-
summaryCalls: 0,
|
|
524
|
-
summaryInputTokens: 0,
|
|
525
|
-
summaryOutputTokens: 0,
|
|
526
|
-
summaryModel: "",
|
|
527
|
-
summaryText: existingSummary ?? "",
|
|
528
|
-
reason: didTruncate
|
|
529
|
-
? "truncated tool results without summarization"
|
|
530
|
-
: "conversation already fits within the compaction target",
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const compactableMessages = messages.slice(
|
|
535
|
-
summaryOffset,
|
|
536
|
-
keepPlan.keepFromIndex,
|
|
537
|
-
);
|
|
538
|
-
if (compactableMessages.length === 0) {
|
|
539
|
-
return {
|
|
540
|
-
messages,
|
|
541
|
-
compacted: false,
|
|
542
|
-
previousEstimatedInputTokens,
|
|
543
|
-
estimatedInputTokens: previousEstimatedInputTokens,
|
|
544
|
-
maxInputTokens: this.config.maxInputTokens,
|
|
545
|
-
thresholdTokens,
|
|
546
|
-
compactedMessages: 0,
|
|
547
|
-
compactedPersistedMessages: 0,
|
|
548
|
-
summaryCalls: 0,
|
|
549
|
-
summaryInputTokens: 0,
|
|
550
|
-
summaryOutputTokens: 0,
|
|
551
|
-
summaryModel: "",
|
|
552
|
-
summaryText: existingSummary ?? "",
|
|
553
|
-
reason: "no eligible messages to compact",
|
|
554
|
-
};
|
|
555
|
-
}
|
|
309
|
+
force: options?.force,
|
|
310
|
+
signal,
|
|
311
|
+
overrideProfile: options?.overrideProfile ?? null,
|
|
312
|
+
nonPersistedPrefixCount: this.nonPersistedPrefixCount,
|
|
313
|
+
};
|
|
556
314
|
|
|
557
|
-
|
|
558
|
-
// contributes 1 to `nonPersistedPrefixCount` but is excluded from
|
|
559
|
-
// `compactableMessages` by `summaryOffset`; subtract it here so the
|
|
560
|
-
// remaining injected count lines up with compactableMessages. A summary
|
|
561
|
-
// produced by this conversation's own prior compaction is not part of
|
|
562
|
-
// `nonPersistedPrefixCount` (already decremented), so no subtraction.
|
|
563
|
-
const injectedSummaryOffset = this.summaryIsInjected ? summaryOffset : 0;
|
|
564
|
-
const injectedInCompactable = Math.min(
|
|
565
|
-
Math.max(0, this.nonPersistedPrefixCount - injectedSummaryOffset),
|
|
566
|
-
compactableMessages.length,
|
|
567
|
-
);
|
|
568
|
-
const compactedPersistedMessages =
|
|
569
|
-
countPersistedMessages(compactableMessages) - injectedInCompactable;
|
|
570
|
-
const rawProjectedMessages = [
|
|
571
|
-
createContextSummaryMessage(existingSummary ?? "Projected summary"),
|
|
572
|
-
...messages.slice(keepPlan.keepFromIndex),
|
|
573
|
-
];
|
|
574
|
-
const { messages: projectedMessages } = truncateToolResultsAcrossHistory(
|
|
575
|
-
rawProjectedMessages,
|
|
576
|
-
COMPACTION_TOOL_RESULT_MAX_CHARS,
|
|
577
|
-
);
|
|
578
|
-
const projectedInputTokens = estimatePromptTokens(
|
|
579
|
-
projectedMessages,
|
|
580
|
-
this.systemPrompt,
|
|
581
|
-
{
|
|
582
|
-
providerName: this.estimationProviderName,
|
|
583
|
-
toolTokenBudget: this.toolTokenBudget,
|
|
584
|
-
},
|
|
585
|
-
);
|
|
586
|
-
const projectedGainTokens = Math.max(
|
|
587
|
-
0,
|
|
588
|
-
previousEstimatedInputTokens - projectedInputTokens,
|
|
589
|
-
);
|
|
590
|
-
const severePressure =
|
|
591
|
-
previousEstimatedInputTokens >=
|
|
592
|
-
Math.floor(this.config.maxInputTokens * SEVERE_PRESSURE_RATIO);
|
|
593
|
-
const lastCompactedAt = options?.lastCompactedAt;
|
|
315
|
+
const result = await runAssistantDrivenCompaction(args);
|
|
594
316
|
|
|
595
|
-
|
|
596
|
-
// sooner. Scale the cooldown inversely with the growth-rate multiplier, capped at
|
|
597
|
-
// 1/4 of the base cooldown so we never check more than 4× as frequently.
|
|
598
|
-
const growthRateMultiplier = Math.max(
|
|
599
|
-
1,
|
|
600
|
-
projectedGainTokens / MIN_GAIN_TOKENS_DURING_COOLDOWN,
|
|
601
|
-
);
|
|
602
|
-
const adaptiveCooldownMs = Math.max(
|
|
603
|
-
COMPACTION_COOLDOWN_MS / 4,
|
|
604
|
-
COMPACTION_COOLDOWN_MS / growthRateMultiplier,
|
|
605
|
-
);
|
|
606
|
-
const withinCooldown =
|
|
607
|
-
typeof lastCompactedAt === "number" &&
|
|
608
|
-
Date.now() - lastCompactedAt < adaptiveCooldownMs;
|
|
317
|
+
if (!result.compacted) return result;
|
|
609
318
|
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
319
|
+
// Recompute the post-compaction token estimate now that the message
|
|
320
|
+
// array has been rebuilt. The compactor returns a conservative
|
|
321
|
+
// placeholder; the agent loop wants the real number for its next
|
|
322
|
+
// budget decision.
|
|
323
|
+
let estimatedInputTokens = result.estimatedInputTokens;
|
|
324
|
+
try {
|
|
325
|
+
estimatedInputTokens = estimatePromptTokens(
|
|
326
|
+
result.messages,
|
|
327
|
+
this.systemPrompt,
|
|
619
328
|
{
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
growthRateMultiplier,
|
|
623
|
-
msSinceCompaction:
|
|
624
|
-
typeof lastCompactedAt === "number"
|
|
625
|
-
? Date.now() - lastCompactedAt
|
|
626
|
-
: null,
|
|
329
|
+
providerName: this.estimationProviderName,
|
|
330
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
627
331
|
},
|
|
628
|
-
"Compaction cooldown active",
|
|
629
332
|
);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
compacted: false,
|
|
633
|
-
previousEstimatedInputTokens,
|
|
634
|
-
estimatedInputTokens: previousEstimatedInputTokens,
|
|
635
|
-
maxInputTokens: this.config.maxInputTokens,
|
|
636
|
-
thresholdTokens,
|
|
637
|
-
compactedMessages: 0,
|
|
638
|
-
compactedPersistedMessages: 0,
|
|
639
|
-
summaryCalls: 0,
|
|
640
|
-
summaryInputTokens: 0,
|
|
641
|
-
summaryOutputTokens: 0,
|
|
642
|
-
summaryModel: "",
|
|
643
|
-
summaryText: existingSummary ?? "",
|
|
644
|
-
reason: "compaction cooldown active",
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (
|
|
649
|
-
compactedPersistedMessages < MIN_COMPACTABLE_PERSISTED_MESSAGES &&
|
|
650
|
-
!severePressure
|
|
651
|
-
) {
|
|
652
|
-
return {
|
|
653
|
-
messages,
|
|
654
|
-
compacted: false,
|
|
655
|
-
previousEstimatedInputTokens,
|
|
656
|
-
estimatedInputTokens: previousEstimatedInputTokens,
|
|
657
|
-
maxInputTokens: this.config.maxInputTokens,
|
|
658
|
-
thresholdTokens,
|
|
659
|
-
compactedMessages: 0,
|
|
660
|
-
compactedPersistedMessages: 0,
|
|
661
|
-
summaryCalls: 0,
|
|
662
|
-
summaryInputTokens: 0,
|
|
663
|
-
summaryOutputTokens: 0,
|
|
664
|
-
summaryModel: "",
|
|
665
|
-
summaryText: existingSummary ?? "",
|
|
666
|
-
reason: "insufficient compactable persisted messages",
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const retainedThreadRefs = collectRetainedThreadReferences(
|
|
671
|
-
messages.slice(keepPlan.keepFromIndex),
|
|
672
|
-
);
|
|
673
|
-
// Strip runtime injections (memory, turn context, workspace hints, etc.)
|
|
674
|
-
// from the messages fed to the summarizer. These blocks are system
|
|
675
|
-
// metadata; leaving them in causes the summary to echo rotating memory
|
|
676
|
-
// content instead of the actual conversation. The caller's live message
|
|
677
|
-
// array is untouched so prefix caching stays intact.
|
|
678
|
-
const transcriptSource = stripCompactionOnlyInjections(compactableMessages);
|
|
679
|
-
const transcriptBlocks = this.capTranscriptBlocksToTokenBudget(
|
|
680
|
-
serializeMessagesToContentBlocks(transcriptSource),
|
|
681
|
-
existingSummary ?? "No previous summary.",
|
|
682
|
-
retainedThreadRefs,
|
|
683
|
-
);
|
|
684
|
-
const summaryUpdate = await this.updateSummary(
|
|
685
|
-
existingSummary ?? "No previous summary.",
|
|
686
|
-
transcriptBlocks,
|
|
687
|
-
retainedThreadRefs,
|
|
688
|
-
signal,
|
|
689
|
-
options?.overrideProfile ?? null,
|
|
690
|
-
);
|
|
691
|
-
const summary = summaryUpdate.summary;
|
|
692
|
-
const summaryInputTokens = summaryUpdate.inputTokens;
|
|
693
|
-
const summaryOutputTokens = summaryUpdate.outputTokens;
|
|
694
|
-
const summaryModel = summaryUpdate.model;
|
|
695
|
-
const summaryCacheCreationInputTokens =
|
|
696
|
-
summaryUpdate.cacheCreationInputTokens;
|
|
697
|
-
const summaryCacheReadInputTokens = summaryUpdate.cacheReadInputTokens;
|
|
698
|
-
const summaryFailed = summaryUpdate.failed;
|
|
699
|
-
const summaryRawResponses: unknown[] = [];
|
|
700
|
-
if (Array.isArray(summaryUpdate.rawResponse)) {
|
|
701
|
-
summaryRawResponses.push(...summaryUpdate.rawResponse);
|
|
702
|
-
} else if (summaryUpdate.rawResponse !== undefined) {
|
|
703
|
-
summaryRawResponses.push(summaryUpdate.rawResponse);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
log.warn({ err }, "Post-compaction token estimate failed");
|
|
704
335
|
}
|
|
705
|
-
const summaryCalls = 1;
|
|
706
|
-
|
|
707
|
-
// Media (images, files) in kept turns is preserved naturally — those
|
|
708
|
-
// turns are carried forward as-is and their token cost is already
|
|
709
|
-
// accounted for by pickKeepBoundary's estimatePromptTokens call.
|
|
710
|
-
// Images in compacted turns are passed to the summarizer so it can
|
|
711
|
-
// describe their visual content in the summary text.
|
|
712
|
-
const summaryMessage = createContextSummaryMessage(summary);
|
|
713
336
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const compactedMessages = [summaryMessage, ...truncatedKeptMessages];
|
|
720
|
-
const estimatedInputTokens = estimatePromptTokens(
|
|
721
|
-
compactedMessages,
|
|
722
|
-
this.systemPrompt,
|
|
723
|
-
{
|
|
724
|
-
providerName: this.estimationProviderName,
|
|
725
|
-
toolTokenBudget: this.toolTokenBudget,
|
|
726
|
-
},
|
|
337
|
+
// Consume any non-persisted prefix messages that were compacted away
|
|
338
|
+
// and clear the injected-summary flag.
|
|
339
|
+
const compactedAway = Math.min(
|
|
340
|
+
this.nonPersistedPrefixCount,
|
|
341
|
+
result.compactedMessages,
|
|
727
342
|
);
|
|
728
|
-
// Consume the injected prefix messages that were compacted away. When the
|
|
729
|
-
// parent-injected summary was replaced by a freshly produced child summary,
|
|
730
|
-
// also consume its slot (it was excluded from injectedInCompactable via
|
|
731
|
-
// injectedSummaryOffset) and clear the flag so subsequent compactions treat
|
|
732
|
-
// the summary at index 0 as child-owned.
|
|
733
343
|
this.nonPersistedPrefixCount = Math.max(
|
|
734
344
|
0,
|
|
735
|
-
this.nonPersistedPrefixCount -
|
|
736
|
-
injectedInCompactable -
|
|
737
|
-
injectedSummaryOffset,
|
|
345
|
+
this.nonPersistedPrefixCount - compactedAway,
|
|
738
346
|
);
|
|
739
347
|
this.summaryIsInjected = false;
|
|
740
348
|
|
|
741
|
-
|
|
742
|
-
{
|
|
743
|
-
previousEstimatedInputTokens,
|
|
744
|
-
estimatedInputTokens,
|
|
745
|
-
compactedMessages: compactableMessages.length,
|
|
746
|
-
compactedPersistedMessages,
|
|
747
|
-
keepTurns: keepPlan.keepTurns,
|
|
748
|
-
summaryCalls,
|
|
749
|
-
},
|
|
750
|
-
"Compacted conversation context window",
|
|
751
|
-
);
|
|
752
|
-
|
|
753
|
-
return {
|
|
754
|
-
messages: compactedMessages,
|
|
755
|
-
compacted: true,
|
|
756
|
-
previousEstimatedInputTokens,
|
|
757
|
-
estimatedInputTokens,
|
|
758
|
-
maxInputTokens: this.config.maxInputTokens,
|
|
759
|
-
thresholdTokens,
|
|
760
|
-
compactedMessages: compactableMessages.length,
|
|
761
|
-
compactedPersistedMessages,
|
|
762
|
-
summaryCalls,
|
|
763
|
-
summaryInputTokens,
|
|
764
|
-
summaryOutputTokens,
|
|
765
|
-
summaryModel,
|
|
766
|
-
summaryCallSite: CONVERSATION_SUMMARY_CALL_SITE,
|
|
767
|
-
summaryOverrideProfile: options?.overrideProfile ?? null,
|
|
768
|
-
summaryCacheCreationInputTokens,
|
|
769
|
-
summaryCacheReadInputTokens,
|
|
770
|
-
summaryRawResponses,
|
|
771
|
-
summaryText: summary,
|
|
772
|
-
summaryFailed,
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
private get targetInputTokens(): number {
|
|
777
|
-
return Math.floor(
|
|
778
|
-
this.config.maxInputTokens *
|
|
779
|
-
(this.config.targetBudgetRatio - this.config.summaryBudgetRatio),
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
private pickKeepBoundary(
|
|
784
|
-
messages: Message[],
|
|
785
|
-
userTurnStarts: number[],
|
|
786
|
-
opts?: {
|
|
787
|
-
minKeepRecentUserTurns?: number;
|
|
788
|
-
targetInputTokensOverride?: number;
|
|
789
|
-
conversationOriginChannel?: string;
|
|
790
|
-
force?: boolean;
|
|
791
|
-
previousEstimatedInputTokens?: number;
|
|
792
|
-
},
|
|
793
|
-
): { keepFromIndex: number; keepTurns: number } {
|
|
794
|
-
// Slack-originated conversations rely on multi-turn thread context
|
|
795
|
-
// (reply chains, quoted messages, contextual references). Bump the
|
|
796
|
-
// default keep floor for them so compaction does not summarize away
|
|
797
|
-
// recent turns that the next reply may directly cite. Explicit
|
|
798
|
-
// `minKeepRecentUserTurns` (including emergency `0`) wins.
|
|
799
|
-
const defaultTurns = opts?.conversationOriginChannel === "slack" ? 8 : 1;
|
|
800
|
-
const minFloor = Math.min(
|
|
801
|
-
Math.max(0, Math.floor(opts?.minKeepRecentUserTurns ?? defaultTurns)),
|
|
802
|
-
userTurnStarts.length,
|
|
803
|
-
);
|
|
804
|
-
const targetTokens = Math.min(
|
|
805
|
-
opts?.targetInputTokensOverride ?? this.targetInputTokens,
|
|
806
|
-
this.targetInputTokens,
|
|
807
|
-
);
|
|
808
|
-
|
|
809
|
-
// Binary search for the maximum keepTurns whose projected tokens fit
|
|
810
|
-
// within the budget. Token count is monotonically non-decreasing with
|
|
811
|
-
// keepTurns (more turns = more tokens), so binary search is valid.
|
|
812
|
-
const projectedTokensForKeep = (turns: number): number => {
|
|
813
|
-
const fromIndex =
|
|
814
|
-
turns === 0
|
|
815
|
-
? messages.length
|
|
816
|
-
: (userTurnStarts[userTurnStarts.length - turns] ?? messages.length);
|
|
817
|
-
const rawProjected = [
|
|
818
|
-
createContextSummaryMessage("Projected summary"),
|
|
819
|
-
...messages.slice(fromIndex),
|
|
820
|
-
];
|
|
821
|
-
const { messages: projectedMessages } = truncateToolResultsAcrossHistory(
|
|
822
|
-
rawProjected,
|
|
823
|
-
COMPACTION_TOOL_RESULT_MAX_CHARS,
|
|
824
|
-
);
|
|
825
|
-
return estimatePromptTokens(projectedMessages, this.systemPrompt, {
|
|
826
|
-
providerName: this.estimationProviderName,
|
|
827
|
-
toolTokenBudget: this.toolTokenBudget,
|
|
828
|
-
});
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
let lo = minFloor;
|
|
832
|
-
let hi = userTurnStarts.length;
|
|
833
|
-
|
|
834
|
-
// Fast path: if keeping all turns already fits, skip the search.
|
|
835
|
-
if (hi > lo && projectedTokensForKeep(hi) > targetTokens) {
|
|
836
|
-
// Binary search: find the largest keepTurns where projected tokens fit.
|
|
837
|
-
while (lo < hi) {
|
|
838
|
-
const mid = lo + Math.ceil((hi - lo) / 2);
|
|
839
|
-
if (projectedTokensForKeep(mid) <= targetTokens) {
|
|
840
|
-
lo = mid;
|
|
841
|
-
} else {
|
|
842
|
-
hi = mid - 1;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
} else {
|
|
846
|
-
lo = hi;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// Under forced compaction with only the implicit default floor in play,
|
|
850
|
-
// that floor stops being an absolute override when the kept region still
|
|
851
|
-
// exceeds the target. Walk keepTurns below the floor — down to 0 if
|
|
852
|
-
// needed — so /compact can always drive the conversation toward target,
|
|
853
|
-
// even when the floor turn itself is oversized (e.g. a huge paste in the
|
|
854
|
-
// last user message). Exceptions that still treat the floor as hard:
|
|
855
|
-
// - Explicit `minKeepRecentUserTurns` (the caller opted in to that
|
|
856
|
-
// floor; emergency recovery already passes 0 when it wants to go all
|
|
857
|
-
// the way down).
|
|
858
|
-
// - Slack origin (the bumped 8-turn floor protects thread reply chains
|
|
859
|
-
// and quoted-message context that the next reply may directly cite).
|
|
860
|
-
// Automatic mid-loop compaction (force !== true) always honors the floor
|
|
861
|
-
// so the in-flight agent turn isn't summarized away.
|
|
862
|
-
const floorIsImplicitDefault =
|
|
863
|
-
opts?.minKeepRecentUserTurns === undefined &&
|
|
864
|
-
opts?.conversationOriginChannel !== "slack";
|
|
865
|
-
if (
|
|
866
|
-
opts?.force &&
|
|
867
|
-
floorIsImplicitDefault &&
|
|
868
|
-
projectedTokensForKeep(lo) > targetTokens
|
|
869
|
-
) {
|
|
870
|
-
while (lo > 0 && projectedTokensForKeep(lo) > targetTokens) {
|
|
871
|
-
lo--;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// The projection's summary-swap and tool_result truncation can make
|
|
876
|
-
// projectedTokensForKeep(hi) optimistically fit even when the live
|
|
877
|
-
// conversation is well over target — sending /compact through the
|
|
878
|
-
// "already fits" skip path as a no-op. Clamp lo so summarization runs.
|
|
879
|
-
if (
|
|
880
|
-
opts?.force &&
|
|
881
|
-
floorIsImplicitDefault &&
|
|
882
|
-
lo === userTurnStarts.length &&
|
|
883
|
-
lo > 0 &&
|
|
884
|
-
(opts?.previousEstimatedInputTokens ?? 0) > targetTokens
|
|
885
|
-
) {
|
|
886
|
-
lo -= 1;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const keepTurns = lo;
|
|
890
|
-
const rawKeepFromIndex =
|
|
891
|
-
keepTurns === 0
|
|
892
|
-
? messages.length
|
|
893
|
-
: (userTurnStarts[userTurnStarts.length - keepTurns] ??
|
|
894
|
-
messages.length);
|
|
895
|
-
const keepFromIndex = adjustForToolPairs(messages, rawKeepFromIndex);
|
|
896
|
-
return { keepFromIndex, keepTurns };
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
private get summaryMaxTokens(): number {
|
|
900
|
-
return Math.max(
|
|
901
|
-
1,
|
|
902
|
-
Math.floor(this.config.maxInputTokens * this.config.summaryBudgetRatio),
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
/**
|
|
907
|
-
* Trim the serialized transcript content blocks so that the summary prompt
|
|
908
|
-
* (system prompt + existing summary + transcript + scaffolding) fits within
|
|
909
|
-
* the provider's input token limit, minus the output budget reserved for the
|
|
910
|
-
* summary itself.
|
|
911
|
-
*
|
|
912
|
-
* When the transcript exceeds the budget, blocks are dropped from the
|
|
913
|
-
* beginning (oldest messages first) to preserve recent context. Image blocks
|
|
914
|
-
* are dropped before text blocks within each pass since they are expensive
|
|
915
|
-
* and their surrounding text context already captures the conversation flow.
|
|
916
|
-
*/
|
|
917
|
-
private capTranscriptBlocksToTokenBudget(
|
|
918
|
-
blocks: ContentBlock[],
|
|
919
|
-
currentSummary: string,
|
|
920
|
-
retainedThreadRefs: string[],
|
|
921
|
-
): ContentBlock[] {
|
|
922
|
-
const retainedRefsText = retainedThreadRefs.join("\n");
|
|
923
|
-
const overheadTokens =
|
|
924
|
-
estimateTextTokens(SUMMARY_SYSTEM_PROMPT) +
|
|
925
|
-
estimateTextTokens(currentSummary) +
|
|
926
|
-
estimateTextTokens(retainedRefsText) +
|
|
927
|
-
// Scaffolding text in buildSummaryContentBlocks ("Update the summary...",
|
|
928
|
-
// section headers, etc.) — generous fixed estimate.
|
|
929
|
-
200 +
|
|
930
|
-
this.summaryMaxTokens;
|
|
931
|
-
|
|
932
|
-
const maxTranscriptTokens = Math.max(
|
|
933
|
-
0,
|
|
934
|
-
this.config.maxInputTokens - overheadTokens,
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
const estimateBlockTokens = (b: ContentBlock): number =>
|
|
938
|
-
estimateContentBlockTokens(b, {
|
|
939
|
-
providerName: this.estimationProviderName,
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
let totalTokens = 0;
|
|
943
|
-
for (const block of blocks) {
|
|
944
|
-
totalTokens += estimateBlockTokens(block);
|
|
945
|
-
}
|
|
946
|
-
const originalTotalTokens = totalTokens;
|
|
947
|
-
if (totalTokens <= maxTranscriptTokens) return blocks;
|
|
948
|
-
|
|
949
|
-
// First pass: drop images from the beginning until we fit or run out of
|
|
950
|
-
// images to drop. Images are high-cost and their text context (message
|
|
951
|
-
// headers, surrounding tool_use/tool_result serializations) is preserved.
|
|
952
|
-
const result = [...blocks];
|
|
953
|
-
for (
|
|
954
|
-
let i = 0;
|
|
955
|
-
i < result.length && totalTokens > maxTranscriptTokens;
|
|
956
|
-
i++
|
|
957
|
-
) {
|
|
958
|
-
if (result[i].type === "image") {
|
|
959
|
-
totalTokens -= estimateBlockTokens(result[i]);
|
|
960
|
-
const stub: ContentBlock = {
|
|
961
|
-
type: "text",
|
|
962
|
-
text: `[image omitted from summary context]`,
|
|
963
|
-
};
|
|
964
|
-
totalTokens += estimateBlockTokens(stub);
|
|
965
|
-
result[i] = stub;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
if (totalTokens <= maxTranscriptTokens) return result;
|
|
969
|
-
|
|
970
|
-
// Second pass: drop text blocks from the beginning (oldest) until we fit.
|
|
971
|
-
// If a single text block exceeds the remaining budget, truncate it rather
|
|
972
|
-
// than dropping it entirely so the summarizer always has content to work with.
|
|
973
|
-
let dropUntil = 0;
|
|
974
|
-
let droppedTokens = 0;
|
|
975
|
-
for (
|
|
976
|
-
let i = 0;
|
|
977
|
-
i < result.length && totalTokens > maxTranscriptTokens;
|
|
978
|
-
i++
|
|
979
|
-
) {
|
|
980
|
-
const blockTokens = estimateBlockTokens(result[i]);
|
|
981
|
-
const excess = totalTokens - maxTranscriptTokens;
|
|
982
|
-
if (blockTokens > excess && result[i].type === "text") {
|
|
983
|
-
// Truncate this block to shed exactly the excess tokens.
|
|
984
|
-
// Subtract the cost of the "[...truncated] " prefix so the final
|
|
985
|
-
// block (prefix + kept text) stays within budget.
|
|
986
|
-
const truncationPrefix = "[...truncated] ";
|
|
987
|
-
const prefixTokens = estimateTextTokens(truncationPrefix);
|
|
988
|
-
const keepTokens = Math.max(1, blockTokens - excess - prefixTokens);
|
|
989
|
-
const text = (result[i] as { type: "text"; text: string }).text;
|
|
990
|
-
// Approximate: 1 token ≈ 4 characters for truncation purposes.
|
|
991
|
-
const keepChars = Math.max(1, Math.floor(keepTokens * 4));
|
|
992
|
-
const truncatedText = text.slice(-keepChars);
|
|
993
|
-
const truncatedBlock: ContentBlock = {
|
|
994
|
-
type: "text",
|
|
995
|
-
text: `${truncationPrefix}${truncatedText}`,
|
|
996
|
-
};
|
|
997
|
-
const newBlockTokens = estimateBlockTokens(truncatedBlock);
|
|
998
|
-
droppedTokens += blockTokens - newBlockTokens;
|
|
999
|
-
totalTokens -= blockTokens - newBlockTokens;
|
|
1000
|
-
result[i] = truncatedBlock;
|
|
1001
|
-
dropUntil = i;
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
droppedTokens += blockTokens;
|
|
1005
|
-
totalTokens -= blockTokens;
|
|
1006
|
-
dropUntil = i + 1;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
log.info(
|
|
1010
|
-
{
|
|
1011
|
-
originalTokens: originalTotalTokens,
|
|
1012
|
-
cappedTokens: maxTranscriptTokens,
|
|
1013
|
-
droppedTokens,
|
|
1014
|
-
},
|
|
1015
|
-
"Capped summary transcript blocks to fit provider input limit",
|
|
1016
|
-
);
|
|
1017
|
-
|
|
1018
|
-
return [
|
|
1019
|
-
{ type: "text", text: "[earlier messages truncated]" } as ContentBlock,
|
|
1020
|
-
...result.slice(dropUntil),
|
|
1021
|
-
];
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
private async updateSummary(
|
|
1025
|
-
currentSummary: string,
|
|
1026
|
-
transcriptBlocks: ContentBlock[],
|
|
1027
|
-
retainedThreadRefs: string[],
|
|
1028
|
-
signal?: AbortSignal,
|
|
1029
|
-
overrideProfile?: string | null,
|
|
1030
|
-
): Promise<{
|
|
1031
|
-
summary: string;
|
|
1032
|
-
inputTokens: number;
|
|
1033
|
-
outputTokens: number;
|
|
1034
|
-
model: string;
|
|
1035
|
-
cacheCreationInputTokens: number;
|
|
1036
|
-
cacheReadInputTokens: number;
|
|
1037
|
-
rawResponse?: unknown;
|
|
1038
|
-
/**
|
|
1039
|
-
* True when the provider.sendMessage call threw and the local fallback
|
|
1040
|
-
* was used. Callers (the agent loop) use this to drive circuit-breaker
|
|
1041
|
-
* state without having to reimplement the fallback themselves.
|
|
1042
|
-
*/
|
|
1043
|
-
failed: boolean;
|
|
1044
|
-
}> {
|
|
1045
|
-
// When the existing summary is already consuming most of its budget,
|
|
1046
|
-
// nudge the model to compress older durable content aggressively so
|
|
1047
|
-
// incremental-update passes don't let the summary grow unboundedly.
|
|
1048
|
-
const existingSummaryTokens = estimateTextTokens(currentSummary);
|
|
1049
|
-
const compressionPressure =
|
|
1050
|
-
existingSummaryTokens >=
|
|
1051
|
-
this.summaryMaxTokens * SUMMARY_COMPRESSION_PRESSURE_RATIO;
|
|
1052
|
-
const contentBlocks = buildSummaryContentBlocks(
|
|
1053
|
-
currentSummary,
|
|
1054
|
-
transcriptBlocks,
|
|
1055
|
-
retainedThreadRefs,
|
|
1056
|
-
{ compressionPressure },
|
|
1057
|
-
);
|
|
1058
|
-
const summaryMessage: Message = { role: "user", content: contentBlocks };
|
|
1059
|
-
let failed = false;
|
|
1060
|
-
try {
|
|
1061
|
-
const providerConfig: Record<string, unknown> = {
|
|
1062
|
-
callSite: CONVERSATION_SUMMARY_CALL_SITE,
|
|
1063
|
-
usageTracking: "manual",
|
|
1064
|
-
max_tokens: this.summaryMaxTokens,
|
|
1065
|
-
};
|
|
1066
|
-
if (overrideProfile) {
|
|
1067
|
-
providerConfig.overrideProfile = overrideProfile;
|
|
1068
|
-
}
|
|
1069
|
-
const response = await this.provider.sendMessage(
|
|
1070
|
-
[summaryMessage],
|
|
1071
|
-
undefined,
|
|
1072
|
-
SUMMARY_SYSTEM_PROMPT,
|
|
1073
|
-
{
|
|
1074
|
-
config: providerConfig,
|
|
1075
|
-
signal,
|
|
1076
|
-
},
|
|
1077
|
-
);
|
|
1078
|
-
|
|
1079
|
-
const nextSummary = extractText(response.content).trim();
|
|
1080
|
-
if (nextSummary.length > 0) {
|
|
1081
|
-
return {
|
|
1082
|
-
summary: this.clampSummary(nextSummary),
|
|
1083
|
-
inputTokens: response.usage.inputTokens,
|
|
1084
|
-
outputTokens: response.usage.outputTokens,
|
|
1085
|
-
model: response.model,
|
|
1086
|
-
cacheCreationInputTokens:
|
|
1087
|
-
response.usage.cacheCreationInputTokens ?? 0,
|
|
1088
|
-
cacheReadInputTokens: response.usage.cacheReadInputTokens ?? 0,
|
|
1089
|
-
rawResponse: response.rawResponse,
|
|
1090
|
-
failed: false,
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
} catch (err) {
|
|
1094
|
-
failed = true;
|
|
1095
|
-
log.warn({ err }, "Summary generation failed, using local fallback");
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Fallback: extract text-only transcript for local summary generation.
|
|
1099
|
-
const textTranscript = transcriptBlocks
|
|
1100
|
-
.filter(
|
|
1101
|
-
(b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
|
|
1102
|
-
)
|
|
1103
|
-
.map((b) => b.text)
|
|
1104
|
-
.join("\n\n");
|
|
1105
|
-
|
|
1106
|
-
return {
|
|
1107
|
-
summary: fallbackSummary(currentSummary, textTranscript),
|
|
1108
|
-
inputTokens: 0,
|
|
1109
|
-
outputTokens: 0,
|
|
1110
|
-
model: "",
|
|
1111
|
-
cacheCreationInputTokens: 0,
|
|
1112
|
-
cacheReadInputTokens: 0,
|
|
1113
|
-
failed,
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
private clampSummary(summary: string): string {
|
|
1118
|
-
// Budget in tokens → approximate char limit (4 chars ≈ 1 token).
|
|
1119
|
-
const maxChars = this.summaryMaxTokens * 4;
|
|
1120
|
-
if (summary.length <= maxChars) return summary;
|
|
1121
|
-
return clampSummaryAtSectionBoundary(summary, maxChars);
|
|
349
|
+
return { ...result, estimatedInputTokens };
|
|
1122
350
|
}
|
|
1123
351
|
}
|
|
1124
352
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
* back to a hard character slice when no boundary exists in the safe
|
|
1129
|
-
* region (first half of the budget).
|
|
1130
|
-
*/
|
|
1131
|
-
export function clampSummaryAtSectionBoundary(
|
|
1132
|
-
summary: string,
|
|
1133
|
-
maxChars: number,
|
|
1134
|
-
): string {
|
|
1135
|
-
if (summary.length <= maxChars) return summary;
|
|
1136
|
-
const ELLIPSIS = "...";
|
|
1137
|
-
// Hard limit we must stay under, leaving room for the ellipsis suffix.
|
|
1138
|
-
const cutoff = maxChars - ELLIPSIS.length;
|
|
1139
|
-
if (cutoff <= 0) return ELLIPSIS;
|
|
1140
|
-
const head = safeStringSlice(summary, 0, cutoff);
|
|
1141
|
-
// Find the last `## ` heading at a line start. Require it to be past the
|
|
1142
|
-
// midpoint of the allowed region so we don't drop most of the summary
|
|
1143
|
-
// just to hit a boundary — better to cut mid-section late than to keep
|
|
1144
|
-
// almost nothing.
|
|
1145
|
-
const halfway = Math.floor(cutoff / 2);
|
|
1146
|
-
const boundary = head.lastIndexOf("\n## ");
|
|
1147
|
-
if (boundary >= halfway) {
|
|
1148
|
-
return `${head.slice(0, boundary).trimEnd()}\n${ELLIPSIS}`;
|
|
1149
|
-
}
|
|
1150
|
-
return `${head}${ELLIPSIS}`;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
function collectUserTurnStartIndexes(messages: Message[]): number[] {
|
|
1154
|
-
const starts: number[] = [];
|
|
1155
|
-
for (let i = 0; i < messages.length; i++) {
|
|
1156
|
-
const message = messages[i];
|
|
1157
|
-
if (message.role !== "user") continue;
|
|
1158
|
-
if (getSummaryFromContextMessage(message) != null) continue;
|
|
1159
|
-
if (isToolResultOnly(message)) continue;
|
|
1160
|
-
starts.push(i);
|
|
1161
|
-
}
|
|
1162
|
-
return starts;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
/**
|
|
1166
|
-
* Count messages that have DB counterparts. Context-summary messages are
|
|
1167
|
-
* in-memory-only and excluded; ALL other messages (including tool-result-only
|
|
1168
|
-
* user messages) have a corresponding row in the DB and must be counted so
|
|
1169
|
-
* that `contextCompactedMessageCount` indexes the DB array correctly.
|
|
1170
|
-
*/
|
|
1171
|
-
function countPersistedMessages(messages: Message[]): number {
|
|
1172
|
-
return messages.filter((message) => {
|
|
1173
|
-
return getSummaryFromContextMessage(message) == null;
|
|
1174
|
-
}).length;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
function isSystemNoticeBlock(block: ContentBlock): boolean {
|
|
1178
|
-
if (block.type !== "text") return false;
|
|
1179
|
-
const text = (block as { text?: string }).text ?? "";
|
|
1180
|
-
return (
|
|
1181
|
-
text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
|
|
1182
|
-
);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
/** A user message that contains ONLY tool_result blocks (no text or other content).
|
|
1186
|
-
* System notice text blocks (retry nudges, progress checks) do not count as user content. */
|
|
1187
|
-
function isToolResultOnly(message: Message): boolean {
|
|
1188
|
-
return (
|
|
1189
|
-
message.content.length > 0 &&
|
|
1190
|
-
message.content.every(
|
|
1191
|
-
(block) =>
|
|
1192
|
-
block.type === "tool_result" ||
|
|
1193
|
-
block.type === "web_search_tool_result" ||
|
|
1194
|
-
isSystemNoticeBlock(block),
|
|
1195
|
-
)
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Helpers
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
1198
356
|
|
|
1199
|
-
|
|
1200
|
-
* Walk the keep boundary backward to ensure tool_use/tool_result pairs are
|
|
1201
|
-
* never split across the compaction boundary. If the first kept message is
|
|
1202
|
-
* a user message containing tool_result blocks whose matching tool_use blocks
|
|
1203
|
-
* live in the preceding (compacted-away) assistant message, include that
|
|
1204
|
-
* assistant message in the kept set.
|
|
1205
|
-
*/
|
|
1206
|
-
function adjustForToolPairs(
|
|
357
|
+
function noopResult(
|
|
1207
358
|
messages: Message[],
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (referencedIds.size === 0) break;
|
|
1227
|
-
|
|
1228
|
-
// Check if the preceding assistant message contains matching tool_uses
|
|
1229
|
-
const prev = messages[idx - 1];
|
|
1230
|
-
if (!prev || prev.role !== "assistant") break;
|
|
1231
|
-
|
|
1232
|
-
const hasOrphanedPair = prev.content.some(
|
|
1233
|
-
(block) =>
|
|
1234
|
-
(block.type === "tool_use" || block.type === "server_tool_use") &&
|
|
1235
|
-
"id" in block &&
|
|
1236
|
-
referencedIds.has((block as { id: string }).id),
|
|
1237
|
-
);
|
|
1238
|
-
if (!hasOrphanedPair) break;
|
|
1239
|
-
|
|
1240
|
-
// Include the assistant message
|
|
1241
|
-
idx--;
|
|
1242
|
-
|
|
1243
|
-
// The assistant message may itself be preceded by a tool_result user
|
|
1244
|
-
// message that pairs with an even earlier assistant — continue the check
|
|
1245
|
-
if (idx > 0 && messages[idx - 1]?.role === "user") {
|
|
1246
|
-
idx--;
|
|
1247
|
-
} else {
|
|
1248
|
-
break;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
return idx;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
export function getSummaryFromContextMessage(
|
|
1255
|
-
message: Message | undefined,
|
|
1256
|
-
): string | null {
|
|
1257
|
-
if (!message) return null;
|
|
1258
|
-
const text = extractText(message.content).trim();
|
|
1259
|
-
if (!text.startsWith(CONTEXT_SUMMARY_MARKER)) return null;
|
|
1260
|
-
if (INTERNAL_CONTEXT_SUMMARY_MESSAGES.has(message)) {
|
|
1261
|
-
return stripContextSummaryTags(text);
|
|
1262
|
-
}
|
|
1263
|
-
return null;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
function stripContextSummaryTags(text: string): string {
|
|
1267
|
-
let inner = text.slice(CONTEXT_SUMMARY_MARKER.length);
|
|
1268
|
-
const closeIdx = inner.lastIndexOf("</context_summary>");
|
|
1269
|
-
if (closeIdx !== -1) {
|
|
1270
|
-
inner = inner.slice(0, closeIdx);
|
|
1271
|
-
}
|
|
1272
|
-
return inner.trim();
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
export function createContextSummaryMessage(summary: string): Message {
|
|
1276
|
-
const message: Message = {
|
|
1277
|
-
role: "user",
|
|
1278
|
-
content: [
|
|
1279
|
-
{
|
|
1280
|
-
type: "text",
|
|
1281
|
-
text: `${CONTEXT_SUMMARY_MARKER}\n${summary}\n</context_summary>`,
|
|
1282
|
-
},
|
|
1283
|
-
],
|
|
359
|
+
estimated: number,
|
|
360
|
+
opts: { maxInputTokens: number; thresholdTokens: number; reason: string },
|
|
361
|
+
): ContextWindowResult {
|
|
362
|
+
return {
|
|
363
|
+
messages,
|
|
364
|
+
compacted: false,
|
|
365
|
+
previousEstimatedInputTokens: estimated,
|
|
366
|
+
estimatedInputTokens: estimated,
|
|
367
|
+
maxInputTokens: opts.maxInputTokens,
|
|
368
|
+
thresholdTokens: opts.thresholdTokens,
|
|
369
|
+
compactedMessages: 0,
|
|
370
|
+
compactedPersistedMessages: 0,
|
|
371
|
+
summaryCalls: 0,
|
|
372
|
+
summaryInputTokens: 0,
|
|
373
|
+
summaryOutputTokens: 0,
|
|
374
|
+
summaryModel: "",
|
|
375
|
+
summaryText: getSummaryFromContextMessage(messages[0]) ?? "",
|
|
376
|
+
reason: opts.reason,
|
|
1284
377
|
};
|
|
1285
|
-
INTERNAL_CONTEXT_SUMMARY_MESSAGES.add(message);
|
|
1286
|
-
return message;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* Build content blocks for the summary prompt. Returns a mix of text blocks
|
|
1291
|
-
* (for the scaffolding, existing summary, and serialized non-image content)
|
|
1292
|
-
* and image blocks (preserved from the original messages so the summarizer
|
|
1293
|
-
* can describe what was in them).
|
|
1294
|
-
*/
|
|
1295
|
-
function buildSummaryContentBlocks(
|
|
1296
|
-
currentSummary: string,
|
|
1297
|
-
transcriptBlocks: ContentBlock[],
|
|
1298
|
-
retainedThreadRefs: string[],
|
|
1299
|
-
options: { compressionPressure: boolean } = { compressionPressure: false },
|
|
1300
|
-
): ContentBlock[] {
|
|
1301
|
-
const lines = [
|
|
1302
|
-
"Update the summary with new transcript data.",
|
|
1303
|
-
"If new information conflicts with older notes, keep the most recent and explicit detail.",
|
|
1304
|
-
"Keep all unresolved asks and next steps.",
|
|
1305
|
-
"For any images included below, describe their visual content in the summary so the information is preserved after compaction.",
|
|
1306
|
-
];
|
|
1307
|
-
if (options.compressionPressure) {
|
|
1308
|
-
lines.push(
|
|
1309
|
-
"The existing summary is approaching its token budget. Compress older durable content aggressively (drop detail that is no longer load-bearing, merge bullets, tighten prose) while preserving the most recent turns' nuance.",
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
lines.push(
|
|
1313
|
-
"",
|
|
1314
|
-
"### Existing Summary",
|
|
1315
|
-
currentSummary.trim().length > 0 ? currentSummary.trim() : "None.",
|
|
1316
|
-
"",
|
|
1317
|
-
);
|
|
1318
|
-
if (retainedThreadRefs.length > 0) {
|
|
1319
|
-
lines.push(
|
|
1320
|
-
"### Retained Thread References",
|
|
1321
|
-
"These reply tag lines remain in the live context after compaction. Each `→ Mxxxxxx` cites a parent message by alias; if that parent appears in the Transcript below, preserve its text verbatim.",
|
|
1322
|
-
...retainedThreadRefs.map((ref) => `- ${ref}`),
|
|
1323
|
-
"",
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
lines.push("### Transcript");
|
|
1327
|
-
return [
|
|
1328
|
-
{
|
|
1329
|
-
type: "text",
|
|
1330
|
-
text: lines.join("\n"),
|
|
1331
|
-
} as ContentBlock,
|
|
1332
|
-
...transcriptBlocks,
|
|
1333
|
-
];
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
/**
|
|
1337
|
-
* Scan retained-tail messages for Slack-style reply tag lines that cite a
|
|
1338
|
-
* thread parent via the `→ Mxxxxxx` alias convention. Returns the full tag
|
|
1339
|
-
* line for each match (de-duplicated, order-preserved) so the summarizer
|
|
1340
|
-
* has a concrete list of parents whose text must be preserved verbatim.
|
|
1341
|
-
*
|
|
1342
|
-
* Non-slack conversations and retained tails without any reply markers
|
|
1343
|
-
* produce an empty list — in that case the summarizer is told explicitly
|
|
1344
|
-
* that no verbatim preservation is required.
|
|
1345
|
-
*/
|
|
1346
|
-
function collectRetainedThreadReferences(
|
|
1347
|
-
retainedMessages: Message[],
|
|
1348
|
-
): string[] {
|
|
1349
|
-
const seen = new Set<string>();
|
|
1350
|
-
const out: string[] = [];
|
|
1351
|
-
for (const msg of retainedMessages) {
|
|
1352
|
-
for (const block of msg.content) {
|
|
1353
|
-
if (block.type !== "text") continue;
|
|
1354
|
-
const text = (block as { text: string }).text;
|
|
1355
|
-
for (const line of text.split("\n")) {
|
|
1356
|
-
if (!THREAD_REPLY_REFERENCE_PATTERN.test(line)) continue;
|
|
1357
|
-
const trimmed = line.trim();
|
|
1358
|
-
if (trimmed.length === 0) continue;
|
|
1359
|
-
if (seen.has(trimmed)) continue;
|
|
1360
|
-
seen.add(trimmed);
|
|
1361
|
-
out.push(trimmed);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
return out;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
/**
|
|
1369
|
-
* Serialize messages into a sequence of content blocks. Text-based content
|
|
1370
|
-
* (tool calls, tool results, thinking, etc.) is serialized into text blocks.
|
|
1371
|
-
* Image blocks — both top-level and nested inside tool_result contentBlocks —
|
|
1372
|
-
* are preserved as-is so the summarizer LLM can see them.
|
|
1373
|
-
*/
|
|
1374
|
-
function serializeMessagesToContentBlocks(messages: Message[]): ContentBlock[] {
|
|
1375
|
-
const blocks: ContentBlock[] = [];
|
|
1376
|
-
for (let i = 0; i < messages.length; i++) {
|
|
1377
|
-
const msg = messages[i];
|
|
1378
|
-
const textLines: string[] = [`Message #${i + 1} (${msg.role})`];
|
|
1379
|
-
|
|
1380
|
-
for (const block of msg.content) {
|
|
1381
|
-
if (block.type === "image") {
|
|
1382
|
-
// Flush accumulated text lines before the image.
|
|
1383
|
-
if (textLines.length > 0) {
|
|
1384
|
-
blocks.push({ type: "text", text: textLines.join("\n") });
|
|
1385
|
-
textLines.length = 0;
|
|
1386
|
-
}
|
|
1387
|
-
blocks.push(block);
|
|
1388
|
-
} else if (block.type === "tool_result") {
|
|
1389
|
-
// guard:allow-tool-result-only — web_search_tool_result handled by serializeBlock via else branch
|
|
1390
|
-
// Extract images from tool_result contentBlocks before serializing.
|
|
1391
|
-
const collectedImages: ImageContent[] = [];
|
|
1392
|
-
textLines.push(serializeToolResultBlock(block, collectedImages));
|
|
1393
|
-
if (collectedImages.length > 0) {
|
|
1394
|
-
// Flush text, emit collected images, then continue.
|
|
1395
|
-
if (textLines.length > 0) {
|
|
1396
|
-
blocks.push({ type: "text", text: textLines.join("\n") });
|
|
1397
|
-
textLines.length = 0;
|
|
1398
|
-
}
|
|
1399
|
-
blocks.push(...collectedImages);
|
|
1400
|
-
}
|
|
1401
|
-
} else {
|
|
1402
|
-
textLines.push(serializeBlock(block));
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
// Flush remaining text lines for this message.
|
|
1407
|
-
if (textLines.length > 0) {
|
|
1408
|
-
blocks.push({ type: "text", text: textLines.join("\n") });
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
return blocks;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
/**
|
|
1415
|
-
* Serialize images nested inside tool_result contentBlocks, returning them
|
|
1416
|
-
* as separate content blocks to preserve for the summarizer.
|
|
1417
|
-
*/
|
|
1418
|
-
function serializeToolResultBlock(
|
|
1419
|
-
block: Extract<ContentBlock, { type: "tool_result" }>,
|
|
1420
|
-
collectedImages: ImageContent[],
|
|
1421
|
-
): string {
|
|
1422
|
-
if (block.contentBlocks) {
|
|
1423
|
-
for (const cb of block.contentBlocks) {
|
|
1424
|
-
if (cb.type === "image") {
|
|
1425
|
-
collectedImages.push(cb);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
return `tool_result ${block.tool_use_id}${
|
|
1430
|
-
block.is_error ? " (error)" : ""
|
|
1431
|
-
}: ${clampText(block.content)}`;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
function serializeBlock(block: ContentBlock): string {
|
|
1435
|
-
switch (block.type) {
|
|
1436
|
-
case "text":
|
|
1437
|
-
return `text: ${clampText(block.text)}`;
|
|
1438
|
-
case "tool_use":
|
|
1439
|
-
return `tool_use ${block.name}: ${clampText(stableJson(block.input))}`;
|
|
1440
|
-
case "tool_result":
|
|
1441
|
-
return `tool_result ${block.tool_use_id}${
|
|
1442
|
-
block.is_error ? " (error)" : ""
|
|
1443
|
-
}: ${clampText(block.content)}`;
|
|
1444
|
-
case "image":
|
|
1445
|
-
// Top-level images are handled by serializeMessagesToContentBlocks.
|
|
1446
|
-
// This path is only hit for images in unexpected positions.
|
|
1447
|
-
return `image: ${block.source.media_type}, ${
|
|
1448
|
-
Math.ceil(block.source.data.length / 4) * 3
|
|
1449
|
-
} bytes(base64)`;
|
|
1450
|
-
case "file": {
|
|
1451
|
-
const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
|
|
1452
|
-
const parts = [
|
|
1453
|
-
`file: ${block.source.filename}`,
|
|
1454
|
-
block.source.media_type,
|
|
1455
|
-
`${sizeBytes} bytes(base64)`,
|
|
1456
|
-
];
|
|
1457
|
-
if (block.extracted_text) {
|
|
1458
|
-
parts.push(`text=${clampText(block.extracted_text)}`);
|
|
1459
|
-
}
|
|
1460
|
-
return parts.join(", ");
|
|
1461
|
-
}
|
|
1462
|
-
case "thinking":
|
|
1463
|
-
return `thinking: ${clampText(block.thinking)}`;
|
|
1464
|
-
case "redacted_thinking":
|
|
1465
|
-
return "redacted_thinking";
|
|
1466
|
-
case "server_tool_use":
|
|
1467
|
-
return `server_tool_use ${block.name}: ${clampText(stableJson(block.input))}`;
|
|
1468
|
-
case "web_search_tool_result":
|
|
1469
|
-
return `web_search_tool_result ${block.tool_use_id}`;
|
|
1470
|
-
default:
|
|
1471
|
-
return "unknown_block";
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
function clampText(text: string): string {
|
|
1476
|
-
if (text.length <= MAX_BLOCK_PREVIEW_CHARS) return text;
|
|
1477
|
-
return `${safeStringSlice(text, 0, MAX_BLOCK_PREVIEW_CHARS)}... [truncated ${
|
|
1478
|
-
text.length - MAX_BLOCK_PREVIEW_CHARS
|
|
1479
|
-
} chars]`;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
function fallbackSummary(currentSummary: string, chunk: string): string {
|
|
1483
|
-
const lines = chunk
|
|
1484
|
-
.split("\n")
|
|
1485
|
-
.map((line) => line.trim())
|
|
1486
|
-
.filter((line) => line.length > 0);
|
|
1487
|
-
const recentLines = lines.slice(-120).join("\n");
|
|
1488
|
-
const merged = [
|
|
1489
|
-
currentSummary.trim(),
|
|
1490
|
-
"## Recent Progress",
|
|
1491
|
-
recentLines.length > 0 ? recentLines : "No new details.",
|
|
1492
|
-
]
|
|
1493
|
-
.filter((part) => part.length > 0)
|
|
1494
|
-
.join("\n\n");
|
|
1495
|
-
if (merged.length <= MAX_FALLBACK_SUMMARY_CHARS) return merged;
|
|
1496
|
-
return merged.slice(merged.length - MAX_FALLBACK_SUMMARY_CHARS);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
function extractText(content: ContentBlock[]): string {
|
|
1500
|
-
return content
|
|
1501
|
-
.filter(
|
|
1502
|
-
(block): block is Extract<ContentBlock, { type: "text" }> =>
|
|
1503
|
-
block.type === "text",
|
|
1504
|
-
)
|
|
1505
|
-
.map((block) => block.text)
|
|
1506
|
-
.join("\n");
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
function stableJson(value: unknown): string {
|
|
1510
|
-
try {
|
|
1511
|
-
return JSON.stringify(value);
|
|
1512
|
-
} catch {
|
|
1513
|
-
return "[unserializable]";
|
|
1514
|
-
}
|
|
1515
378
|
}
|