@vellumai/assistant 0.6.5 → 0.6.6
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 +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
|
@@ -30,6 +30,118 @@ const COMPACTION_TOOL_RESULT_MAX_CHARS = 6_000;
|
|
|
30
30
|
const MIN_COMPACTABLE_PERSISTED_MESSAGES = 2;
|
|
31
31
|
const INTERNAL_CONTEXT_SUMMARY_MESSAGES = new WeakSet<Message>();
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* When the existing summary is this fraction or more of the per-summary
|
|
35
|
+
* token budget, inject a "compress older content aggressively" instruction
|
|
36
|
+
* so incremental-update passes don't let the summary grow unboundedly.
|
|
37
|
+
*/
|
|
38
|
+
const SUMMARY_COMPRESSION_PRESSURE_RATIO = 0.6;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Text-block prefixes that persist in live history (for prefix-caching
|
|
42
|
+
* stability and model grounding) but pollute the summarizer's view of the
|
|
43
|
+
* actual conversation. These blocks are system-metadata attached to user
|
|
44
|
+
* turns — memory injections, turn context, workspace hints, etc. They are
|
|
45
|
+
* stripped ONLY from the messages fed to the summarization LLM call. Live
|
|
46
|
+
* history is never mutated, so prefix caching is preserved.
|
|
47
|
+
*
|
|
48
|
+
* This list intentionally overlaps with `RUNTIME_INJECTION_PREFIXES` in
|
|
49
|
+
* `conversation-runtime-assembly.ts`. That list governs in-flight turn
|
|
50
|
+
* assembly via pure prefix matching; this one governs compaction input.
|
|
51
|
+
* Keep the two lists in sync when a new injection type is added.
|
|
52
|
+
*
|
|
53
|
+
* Compaction strip coverage is two-tier: this prefix list catches
|
|
54
|
+
* internal-vocabulary tags and any tag carrying the `__injected`
|
|
55
|
+
* attribute, while `COMPACTION_ONLY_WRAPPED_STRIP_TAGS` below matches
|
|
56
|
+
* ambiguous bare-tag blocks that are shaped like a runtime-emitted
|
|
57
|
+
* open/close wrap. A new ambiguous tag added upstream needs to be
|
|
58
|
+
* evaluated against both tiers — internal-vocabulary names go here,
|
|
59
|
+
* and names whose bare form collides with ordinary English
|
|
60
|
+
* (`<memory>`, `<workspace>`, `<knowledge_base>`, `<pkb>`,
|
|
61
|
+
* `<system_reminder>`) go in the wrapped-strip list so user prose
|
|
62
|
+
* mentioning the tag is preserved.
|
|
63
|
+
*/
|
|
64
|
+
const COMPACTION_ONLY_STRIP_PREFIXES = [
|
|
65
|
+
"<memory __injected>",
|
|
66
|
+
"<memory_image __injected>",
|
|
67
|
+
"</memory_image>",
|
|
68
|
+
"<memory_context __injected>",
|
|
69
|
+
"<turn_context>",
|
|
70
|
+
"<channel_turn_context>",
|
|
71
|
+
"<guardian_context>",
|
|
72
|
+
"<inbound_actor_context>",
|
|
73
|
+
"<interface_turn_context>",
|
|
74
|
+
"<workspace_top_level>",
|
|
75
|
+
"<now_scratchpad>",
|
|
76
|
+
"<NOW.md Always keep this up to date",
|
|
77
|
+
"<active_thread>",
|
|
78
|
+
"<active_subagents>",
|
|
79
|
+
"<active_workspace>",
|
|
80
|
+
"<active_dynamic_page>",
|
|
81
|
+
"<channel_capabilities>",
|
|
82
|
+
"<channel_command_context>",
|
|
83
|
+
"<voice_call_control>",
|
|
84
|
+
"<transport_hints>",
|
|
85
|
+
"<system_notice>",
|
|
86
|
+
"<non_interactive_context>",
|
|
87
|
+
"<temporal_context>",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tags whose bare form (`<tag>`) is common English vocabulary or markup a
|
|
92
|
+
* user might legitimately type in prose. For these we only strip a text
|
|
93
|
+
* block if it is shaped exactly like a runtime injection: starts with
|
|
94
|
+
* `<tag>\n` and ends with `</tag>`. This bare-tag wrapped shape
|
|
95
|
+
* (e.g. `<memory>\n...\n</memory>`) appears in persisted history
|
|
96
|
+
* alongside the `__injected`-attributed variants, which the prefix list
|
|
97
|
+
* above already catches via `<memory __injected>`. A user who mentions
|
|
98
|
+
* `<memory>` in a sentence or inlines `<workspace>...</workspace>` within
|
|
99
|
+
* other prose will not match this shape.
|
|
100
|
+
*/
|
|
101
|
+
const COMPACTION_ONLY_WRAPPED_STRIP_TAGS = [
|
|
102
|
+
"memory",
|
|
103
|
+
"memory_context",
|
|
104
|
+
"workspace",
|
|
105
|
+
"knowledge_base",
|
|
106
|
+
"pkb",
|
|
107
|
+
"system_reminder",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
function isCompactionInjectedBlock(text: string): boolean {
|
|
111
|
+
if (COMPACTION_ONLY_STRIP_PREFIXES.some((p) => text.startsWith(p))) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return COMPACTION_ONLY_WRAPPED_STRIP_TAGS.some(
|
|
115
|
+
(tag) => text.startsWith(`<${tag}>\n`) && text.endsWith(`</${tag}>`),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove text blocks that look like runtime injections from user messages.
|
|
121
|
+
* Non-text blocks (images, tool_use, tool_result, etc.) are untouched.
|
|
122
|
+
* Empty messages (every block filtered out) are dropped from the output.
|
|
123
|
+
*
|
|
124
|
+
* Used only on the `compactableMessages` slice right before it is
|
|
125
|
+
* serialized for the summarization LLM — the caller's original message
|
|
126
|
+
* array is never mutated.
|
|
127
|
+
*/
|
|
128
|
+
export function stripCompactionOnlyInjections(messages: Message[]): Message[] {
|
|
129
|
+
return messages
|
|
130
|
+
.map((message) => {
|
|
131
|
+
if (message.role !== "user") return message;
|
|
132
|
+
const nextContent = message.content.filter((block) => {
|
|
133
|
+
if (block.type !== "text") return true;
|
|
134
|
+
return !isCompactionInjectedBlock(block.text);
|
|
135
|
+
});
|
|
136
|
+
if (nextContent.length === message.content.length) return message;
|
|
137
|
+
if (nextContent.length === 0) return null;
|
|
138
|
+
return { ...message, content: nextContent };
|
|
139
|
+
})
|
|
140
|
+
.filter(
|
|
141
|
+
(message): message is NonNullable<typeof message> => message != null,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
33
145
|
/**
|
|
34
146
|
* Load the compaction summary system prompt from the bundled markdown asset.
|
|
35
147
|
*
|
|
@@ -57,18 +169,27 @@ export function loadCompactPrompt(): string {
|
|
|
57
169
|
* rather than failing module import at startup.
|
|
58
170
|
*/
|
|
59
171
|
const SUMMARY_PROMPT_FALLBACK = [
|
|
60
|
-
"You
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
172
|
+
"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.",
|
|
173
|
+
"",
|
|
174
|
+
"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.",
|
|
175
|
+
"",
|
|
176
|
+
"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.",
|
|
177
|
+
"",
|
|
178
|
+
"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:",
|
|
179
|
+
"- `## What We're Working On`",
|
|
180
|
+
"- `## Decisions & Commitments`",
|
|
181
|
+
"- `## Facts Worth Remembering`",
|
|
182
|
+
"- `## Open Threads`",
|
|
183
|
+
"- `## Emotional Arc / Relationship Notes` (include when relevant)",
|
|
184
|
+
"- `## Artifacts & References`",
|
|
185
|
+
"",
|
|
186
|
+
"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.",
|
|
187
|
+
"",
|
|
188
|
+
"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.",
|
|
189
|
+
"",
|
|
190
|
+
'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.',
|
|
191
|
+
"",
|
|
192
|
+
"Return only the summary itself in markdown — no preamble, no meta-commentary.",
|
|
72
193
|
].join("\n");
|
|
73
194
|
|
|
74
195
|
/**
|
|
@@ -535,8 +656,14 @@ export class ContextWindowManager {
|
|
|
535
656
|
const retainedThreadRefs = collectRetainedThreadReferences(
|
|
536
657
|
messages.slice(keepPlan.keepFromIndex),
|
|
537
658
|
);
|
|
659
|
+
// Strip runtime injections (memory, turn context, workspace hints, etc.)
|
|
660
|
+
// from the messages fed to the summarizer. These blocks are system
|
|
661
|
+
// metadata; leaving them in causes the summary to echo rotating memory
|
|
662
|
+
// content instead of the actual conversation. The caller's live message
|
|
663
|
+
// array is untouched so prefix caching stays intact.
|
|
664
|
+
const transcriptSource = stripCompactionOnlyInjections(compactableMessages);
|
|
538
665
|
const transcriptBlocks = this.capTranscriptBlocksToTokenBudget(
|
|
539
|
-
serializeMessagesToContentBlocks(
|
|
666
|
+
serializeMessagesToContentBlocks(transcriptSource),
|
|
540
667
|
existingSummary ?? "No previous summary.",
|
|
541
668
|
retainedThreadRefs,
|
|
542
669
|
);
|
|
@@ -882,10 +1009,18 @@ export class ContextWindowManager {
|
|
|
882
1009
|
*/
|
|
883
1010
|
failed: boolean;
|
|
884
1011
|
}> {
|
|
1012
|
+
// When the existing summary is already consuming most of its budget,
|
|
1013
|
+
// nudge the model to compress older durable content aggressively so
|
|
1014
|
+
// incremental-update passes don't let the summary grow unboundedly.
|
|
1015
|
+
const existingSummaryTokens = estimateTextTokens(currentSummary);
|
|
1016
|
+
const compressionPressure =
|
|
1017
|
+
existingSummaryTokens >=
|
|
1018
|
+
this.summaryMaxTokens * SUMMARY_COMPRESSION_PRESSURE_RATIO;
|
|
885
1019
|
const contentBlocks = buildSummaryContentBlocks(
|
|
886
1020
|
currentSummary,
|
|
887
1021
|
transcriptBlocks,
|
|
888
1022
|
retainedThreadRefs,
|
|
1023
|
+
{ compressionPressure },
|
|
889
1024
|
);
|
|
890
1025
|
const summaryMessage: Message = { role: "user", content: contentBlocks };
|
|
891
1026
|
let failed = false;
|
|
@@ -895,7 +1030,10 @@ export class ContextWindowManager {
|
|
|
895
1030
|
undefined,
|
|
896
1031
|
SUMMARY_SYSTEM_PROMPT,
|
|
897
1032
|
{
|
|
898
|
-
config: {
|
|
1033
|
+
config: {
|
|
1034
|
+
callSite: "conversationSummarization" as const,
|
|
1035
|
+
max_tokens: this.summaryMaxTokens,
|
|
1036
|
+
},
|
|
899
1037
|
signal,
|
|
900
1038
|
},
|
|
901
1039
|
);
|
|
@@ -942,10 +1080,38 @@ export class ContextWindowManager {
|
|
|
942
1080
|
// Budget in tokens → approximate char limit (4 chars ≈ 1 token).
|
|
943
1081
|
const maxChars = this.summaryMaxTokens * 4;
|
|
944
1082
|
if (summary.length <= maxChars) return summary;
|
|
945
|
-
return
|
|
1083
|
+
return clampSummaryAtSectionBoundary(summary, maxChars);
|
|
946
1084
|
}
|
|
947
1085
|
}
|
|
948
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* Truncate a markdown summary that exceeds `maxChars`, preferring a
|
|
1089
|
+
* section boundary (`\n## `) so we never cut a heading mid-text. Falls
|
|
1090
|
+
* back to a hard character slice when no boundary exists in the safe
|
|
1091
|
+
* region (first half of the budget).
|
|
1092
|
+
*/
|
|
1093
|
+
export function clampSummaryAtSectionBoundary(
|
|
1094
|
+
summary: string,
|
|
1095
|
+
maxChars: number,
|
|
1096
|
+
): string {
|
|
1097
|
+
if (summary.length <= maxChars) return summary;
|
|
1098
|
+
const ELLIPSIS = "...";
|
|
1099
|
+
// Hard limit we must stay under, leaving room for the ellipsis suffix.
|
|
1100
|
+
const cutoff = maxChars - ELLIPSIS.length;
|
|
1101
|
+
if (cutoff <= 0) return ELLIPSIS;
|
|
1102
|
+
const head = safeStringSlice(summary, 0, cutoff);
|
|
1103
|
+
// Find the last `## ` heading at a line start. Require it to be past the
|
|
1104
|
+
// midpoint of the allowed region so we don't drop most of the summary
|
|
1105
|
+
// just to hit a boundary — better to cut mid-section late than to keep
|
|
1106
|
+
// almost nothing.
|
|
1107
|
+
const halfway = Math.floor(cutoff / 2);
|
|
1108
|
+
const boundary = head.lastIndexOf("\n## ");
|
|
1109
|
+
if (boundary >= halfway) {
|
|
1110
|
+
return `${head.slice(0, boundary).trimEnd()}\n${ELLIPSIS}`;
|
|
1111
|
+
}
|
|
1112
|
+
return `${head}${ELLIPSIS}`;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
949
1115
|
function collectUserTurnStartIndexes(messages: Message[]): number[] {
|
|
950
1116
|
const starts: number[] = [];
|
|
951
1117
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -1092,17 +1258,25 @@ function buildSummaryContentBlocks(
|
|
|
1092
1258
|
currentSummary: string,
|
|
1093
1259
|
transcriptBlocks: ContentBlock[],
|
|
1094
1260
|
retainedThreadRefs: string[],
|
|
1261
|
+
options: { compressionPressure: boolean } = { compressionPressure: false },
|
|
1095
1262
|
): ContentBlock[] {
|
|
1096
1263
|
const lines = [
|
|
1097
1264
|
"Update the summary with new transcript data.",
|
|
1098
1265
|
"If new information conflicts with older notes, keep the most recent and explicit detail.",
|
|
1099
1266
|
"Keep all unresolved asks and next steps.",
|
|
1100
1267
|
"For any images included below, describe their visual content in the summary so the information is preserved after compaction.",
|
|
1268
|
+
];
|
|
1269
|
+
if (options.compressionPressure) {
|
|
1270
|
+
lines.push(
|
|
1271
|
+
"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.",
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
lines.push(
|
|
1101
1275
|
"",
|
|
1102
1276
|
"### Existing Summary",
|
|
1103
1277
|
currentSummary.trim().length > 0 ? currentSummary.trim() : "None.",
|
|
1104
1278
|
"",
|
|
1105
|
-
|
|
1279
|
+
);
|
|
1106
1280
|
if (retainedThreadRefs.length > 0) {
|
|
1107
1281
|
lines.push(
|
|
1108
1282
|
"### Retained Thread References",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
oauthConnectionAccessTokenPath,
|
|
17
17
|
} from "@vellumai/credential-storage";
|
|
18
18
|
|
|
19
|
+
import { manualTokenAccessCredentialKey } from "../oauth/manual-token-connection.js";
|
|
19
20
|
import {
|
|
20
21
|
getProvider,
|
|
21
22
|
listActiveConnectionsByProvider,
|
|
@@ -100,7 +101,9 @@ async function pingProvider(
|
|
|
100
101
|
|
|
101
102
|
const body =
|
|
102
103
|
method !== "GET" && pingBody
|
|
103
|
-
?
|
|
104
|
+
? typeof pingBody === "string"
|
|
105
|
+
? pingBody
|
|
106
|
+
: JSON.stringify(pingBody)
|
|
104
107
|
: undefined;
|
|
105
108
|
|
|
106
109
|
try {
|
|
@@ -155,12 +158,22 @@ async function checkConnection(
|
|
|
155
158
|
pingBody,
|
|
156
159
|
} = opts;
|
|
157
160
|
|
|
158
|
-
const base = {
|
|
161
|
+
const base = {
|
|
162
|
+
connectionId,
|
|
163
|
+
provider,
|
|
164
|
+
accountInfo,
|
|
165
|
+
missingScopes: [] as string[],
|
|
166
|
+
};
|
|
159
167
|
|
|
160
|
-
// 1. Check token presence
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
// 1. Check token presence. Manual-token providers (e.g. slack_channel,
|
|
169
|
+
// telegram) store their primary token under credential/<provider>/<field>
|
|
170
|
+
// rather than oauth_connection/<id>/access_token, so resolve the correct
|
|
171
|
+
// path before looking up the token — otherwise the lookup always returns
|
|
172
|
+
// null and marks these providers as "missing_token".
|
|
173
|
+
const tokenPath =
|
|
174
|
+
manualTokenAccessCredentialKey(provider) ??
|
|
175
|
+
oauthConnectionAccessTokenPath(connectionId);
|
|
176
|
+
const token = await getSecureKeyAsync(tokenPath);
|
|
164
177
|
if (!token) {
|
|
165
178
|
return {
|
|
166
179
|
...base,
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the background conversation feed event emission logic in
|
|
3
|
+
* conversation-agent-loop.ts.
|
|
4
|
+
*
|
|
5
|
+
* Rather than running the full agent loop, these tests exercise the
|
|
6
|
+
* extraction logic in isolation by simulating the conditions under which
|
|
7
|
+
* emitFeedEvent is called: conversation type, message content, and title.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Module-level mocks — must be in place before importing the module under test
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const emitFeedEventSpy = mock<
|
|
17
|
+
(params: {
|
|
18
|
+
source: string;
|
|
19
|
+
title: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
dedupKey?: string;
|
|
22
|
+
}) => Promise<unknown>
|
|
23
|
+
>(async () => ({}));
|
|
24
|
+
|
|
25
|
+
mock.module("../../home/emit-feed-event.js", () => ({
|
|
26
|
+
emitFeedEvent: emitFeedEventSpy,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const getConversationSpy = mock<
|
|
30
|
+
(id: string) => {
|
|
31
|
+
conversationType: string;
|
|
32
|
+
title: string | null;
|
|
33
|
+
} | null
|
|
34
|
+
>(() => null);
|
|
35
|
+
|
|
36
|
+
const getMessageByIdSpy = mock<
|
|
37
|
+
(
|
|
38
|
+
messageId: string,
|
|
39
|
+
conversationId?: string,
|
|
40
|
+
) => { id: string; content: string } | null
|
|
41
|
+
>(() => null);
|
|
42
|
+
|
|
43
|
+
// We need to stub enough of conversation-crud to avoid DB initialization.
|
|
44
|
+
// The actual agent loop imports getConversation and getMessageById from
|
|
45
|
+
// conversation-crud — we intercept those here for test assertions.
|
|
46
|
+
mock.module("../../memory/conversation-crud.js", () => ({
|
|
47
|
+
getConversation: getConversationSpy,
|
|
48
|
+
getMessageById: getMessageByIdSpy,
|
|
49
|
+
// Stubs for other exports that may be transitively referenced:
|
|
50
|
+
addMessage: () => {},
|
|
51
|
+
clearStrippedInjectionMetadataForConversation: () => {},
|
|
52
|
+
deleteMessageById: () => {},
|
|
53
|
+
getConversationOriginChannel: () => null,
|
|
54
|
+
getConversationOriginInterface: () => null,
|
|
55
|
+
getLastUserTimestampBefore: () => null,
|
|
56
|
+
provenanceFromTrustContext: () => ({}),
|
|
57
|
+
updateConversationContextWindow: () => {},
|
|
58
|
+
updateConversationTitle: () => {},
|
|
59
|
+
updateMessageMetadata: () => {},
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Helpers
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Simulates the feed-event emission logic from conversation-agent-loop.ts.
|
|
68
|
+
*
|
|
69
|
+
* This mirrors the block inserted after `message_complete` so we can test it
|
|
70
|
+
* in isolation without spinning up the full agent loop infrastructure.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Tracks the last warning logged by simulateFeedEventEmission, so tests can
|
|
74
|
+
* assert that malformed content is swallowed gracefully.
|
|
75
|
+
*/
|
|
76
|
+
let lastFeedEventWarning: unknown = undefined;
|
|
77
|
+
|
|
78
|
+
function simulateFeedEventEmission(
|
|
79
|
+
conversationId: string,
|
|
80
|
+
lastAssistantMessageId: string | undefined,
|
|
81
|
+
): void {
|
|
82
|
+
lastFeedEventWarning = undefined;
|
|
83
|
+
try {
|
|
84
|
+
const conv = getConversationSpy(conversationId);
|
|
85
|
+
if (
|
|
86
|
+
conv &&
|
|
87
|
+
(conv.conversationType === "background" ||
|
|
88
|
+
conv.conversationType === "scheduled")
|
|
89
|
+
) {
|
|
90
|
+
const lastMsg = lastAssistantMessageId
|
|
91
|
+
? getMessageByIdSpy(lastAssistantMessageId, conversationId)
|
|
92
|
+
: undefined;
|
|
93
|
+
let summary: string;
|
|
94
|
+
if (lastMsg) {
|
|
95
|
+
const parsed: unknown = JSON.parse(lastMsg.content);
|
|
96
|
+
if (typeof parsed === "string") {
|
|
97
|
+
summary = parsed.slice(0, 200);
|
|
98
|
+
} else if (Array.isArray(parsed)) {
|
|
99
|
+
const textBlock = (
|
|
100
|
+
parsed as Array<{ type?: string; text?: string }>
|
|
101
|
+
).find((b) => b.type === "text");
|
|
102
|
+
summary =
|
|
103
|
+
typeof textBlock?.text === "string"
|
|
104
|
+
? textBlock.text.slice(0, 200)
|
|
105
|
+
: (conv.title ?? "Background task completed.");
|
|
106
|
+
} else {
|
|
107
|
+
summary = conv.title ?? "Background task completed.";
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
summary = conv.title ?? "Background task completed.";
|
|
111
|
+
}
|
|
112
|
+
void emitFeedEventSpy({
|
|
113
|
+
source: "assistant",
|
|
114
|
+
title: conv.title ?? "Background Task",
|
|
115
|
+
summary,
|
|
116
|
+
dedupKey: `bg-conv:${conversationId}`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch (feedErr) {
|
|
120
|
+
lastFeedEventWarning = feedErr;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Tests
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
describe("background conversation feed event", () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
emitFeedEventSpy.mockClear();
|
|
131
|
+
getConversationSpy.mockClear();
|
|
132
|
+
getMessageByIdSpy.mockClear();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
emitFeedEventSpy.mockReset();
|
|
137
|
+
getConversationSpy.mockReset();
|
|
138
|
+
getMessageByIdSpy.mockReset();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("emits feed event for background conversation with text content", () => {
|
|
142
|
+
const convId = "conv-bg-123";
|
|
143
|
+
const msgId = "msg-456";
|
|
144
|
+
|
|
145
|
+
getConversationSpy.mockReturnValue({
|
|
146
|
+
conversationType: "background",
|
|
147
|
+
title: "Nightly Cleanup",
|
|
148
|
+
});
|
|
149
|
+
getMessageByIdSpy.mockReturnValue({
|
|
150
|
+
id: msgId,
|
|
151
|
+
content: JSON.stringify([
|
|
152
|
+
{ type: "text", text: "Cleaned up 42 files successfully." },
|
|
153
|
+
]),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
simulateFeedEventEmission(convId, msgId);
|
|
157
|
+
|
|
158
|
+
expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(emitFeedEventSpy).toHaveBeenCalledWith({
|
|
160
|
+
source: "assistant",
|
|
161
|
+
title: "Nightly Cleanup",
|
|
162
|
+
summary: "Cleaned up 42 files successfully.",
|
|
163
|
+
dedupKey: `bg-conv:${convId}`,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("emits feed event for scheduled conversation", () => {
|
|
168
|
+
const convId = "conv-sched-789";
|
|
169
|
+
const msgId = "msg-101";
|
|
170
|
+
|
|
171
|
+
getConversationSpy.mockReturnValue({
|
|
172
|
+
conversationType: "scheduled",
|
|
173
|
+
title: "Weekly Report",
|
|
174
|
+
});
|
|
175
|
+
getMessageByIdSpy.mockReturnValue({
|
|
176
|
+
id: msgId,
|
|
177
|
+
content: JSON.stringify([{ type: "text", text: "Report generated." }]),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
simulateFeedEventEmission(convId, msgId);
|
|
181
|
+
|
|
182
|
+
expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(emitFeedEventSpy).toHaveBeenCalledWith({
|
|
184
|
+
source: "assistant",
|
|
185
|
+
title: "Weekly Report",
|
|
186
|
+
summary: "Report generated.",
|
|
187
|
+
dedupKey: `bg-conv:${convId}`,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("truncates summary to 200 characters", () => {
|
|
192
|
+
const convId = "conv-bg-long";
|
|
193
|
+
const msgId = "msg-long";
|
|
194
|
+
const longText = "A".repeat(300);
|
|
195
|
+
|
|
196
|
+
getConversationSpy.mockReturnValue({
|
|
197
|
+
conversationType: "background",
|
|
198
|
+
title: "Long Task",
|
|
199
|
+
});
|
|
200
|
+
getMessageByIdSpy.mockReturnValue({
|
|
201
|
+
id: msgId,
|
|
202
|
+
content: JSON.stringify([{ type: "text", text: longText }]),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
simulateFeedEventEmission(convId, msgId);
|
|
206
|
+
|
|
207
|
+
expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
|
|
208
|
+
const call = emitFeedEventSpy.mock.calls[0]![0];
|
|
209
|
+
expect(call.summary).toHaveLength(200);
|
|
210
|
+
expect(call.summary).toBe(longText.slice(0, 200));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("falls back to conversation title when no text block in content", () => {
|
|
214
|
+
const convId = "conv-bg-notext";
|
|
215
|
+
const msgId = "msg-notext";
|
|
216
|
+
|
|
217
|
+
getConversationSpy.mockReturnValue({
|
|
218
|
+
conversationType: "background",
|
|
219
|
+
title: "Image Task",
|
|
220
|
+
});
|
|
221
|
+
getMessageByIdSpy.mockReturnValue({
|
|
222
|
+
id: msgId,
|
|
223
|
+
content: JSON.stringify([{ type: "image", source: { data: "..." } }]),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
simulateFeedEventEmission(convId, msgId);
|
|
227
|
+
|
|
228
|
+
expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
|
|
229
|
+
expect(emitFeedEventSpy.mock.calls[0]![0].summary).toBe("Image Task");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("falls back to default summary when no message and no title", () => {
|
|
233
|
+
const convId = "conv-bg-notitle";
|
|
234
|
+
|
|
235
|
+
getConversationSpy.mockReturnValue({
|
|
236
|
+
conversationType: "background",
|
|
237
|
+
title: null,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
simulateFeedEventEmission(convId, undefined);
|
|
241
|
+
|
|
242
|
+
expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
|
|
243
|
+
const call = emitFeedEventSpy.mock.calls[0]![0];
|
|
244
|
+
expect(call.title).toBe("Background Task");
|
|
245
|
+
expect(call.summary).toBe("Background task completed.");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("does NOT emit feed event for standard (foreground) conversations", () => {
|
|
249
|
+
const convId = "conv-standard-1";
|
|
250
|
+
|
|
251
|
+
getConversationSpy.mockReturnValue({
|
|
252
|
+
conversationType: "standard",
|
|
253
|
+
title: "Regular Chat",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
simulateFeedEventEmission(convId, undefined);
|
|
257
|
+
|
|
258
|
+
expect(emitFeedEventSpy).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("does NOT emit feed event for private conversations", () => {
|
|
262
|
+
const convId = "conv-private-1";
|
|
263
|
+
|
|
264
|
+
getConversationSpy.mockReturnValue({
|
|
265
|
+
conversationType: "private",
|
|
266
|
+
title: "Secret Chat",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
simulateFeedEventEmission(convId, undefined);
|
|
270
|
+
|
|
271
|
+
expect(emitFeedEventSpy).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("uses dedupKey per conversation for in-place updates", () => {
|
|
275
|
+
const convId = "conv-bg-dedup";
|
|
276
|
+
|
|
277
|
+
getConversationSpy.mockReturnValue({
|
|
278
|
+
conversationType: "background",
|
|
279
|
+
title: "Recurring Job",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// First run
|
|
283
|
+
simulateFeedEventEmission(convId, undefined);
|
|
284
|
+
// Second run (re-run of same conversation)
|
|
285
|
+
simulateFeedEventEmission(convId, undefined);
|
|
286
|
+
|
|
287
|
+
expect(emitFeedEventSpy).toHaveBeenCalledTimes(2);
|
|
288
|
+
// Both calls use the same dedupKey
|
|
289
|
+
expect(emitFeedEventSpy.mock.calls[0]![0].dedupKey).toBe(
|
|
290
|
+
`bg-conv:${convId}`,
|
|
291
|
+
);
|
|
292
|
+
expect(emitFeedEventSpy.mock.calls[1]![0].dedupKey).toBe(
|
|
293
|
+
`bg-conv:${convId}`,
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("swallows malformed JSON content without throwing", () => {
|
|
298
|
+
const convId = "conv-bg-badjson";
|
|
299
|
+
const msgId = "msg-badjson";
|
|
300
|
+
|
|
301
|
+
getConversationSpy.mockReturnValue({
|
|
302
|
+
conversationType: "background",
|
|
303
|
+
title: "Bad JSON Task",
|
|
304
|
+
});
|
|
305
|
+
getMessageByIdSpy.mockReturnValue({
|
|
306
|
+
id: msgId,
|
|
307
|
+
content: "this is not valid JSON {{{",
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Should not throw
|
|
311
|
+
expect(() => simulateFeedEventEmission(convId, msgId)).not.toThrow();
|
|
312
|
+
// The error was caught and logged as a warning
|
|
313
|
+
expect(lastFeedEventWarning).toBeInstanceOf(SyntaxError);
|
|
314
|
+
// emitFeedEvent should NOT have been called
|
|
315
|
+
expect(emitFeedEventSpy).not.toHaveBeenCalled();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* `auto-analyze` feature flag and source-type guard).
|
|
8
8
|
*
|
|
9
9
|
* We stub the downstream enqueue helpers and the side-effecting lifecycle
|
|
10
|
-
* deps (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* deps (notifier/skill cleanup, browser-screencast) so the test can invoke
|
|
11
|
+
* `disposeConversation` with a minimal `DisposeContext` and assert on the
|
|
12
|
+
* enqueue bookkeeping alone.
|
|
13
13
|
*
|
|
14
14
|
* Two recursion guards apply when the source conversation is itself an
|
|
15
15
|
* auto-analysis conversation:
|
|
@@ -83,19 +83,12 @@ mock.module("../../memory/auto-analysis-enqueue.js", () => ({
|
|
|
83
83
|
|
|
84
84
|
// Stub all side-effecting cleanup helpers that disposeConversation chains
|
|
85
85
|
// into after the enqueue block. We assert on enqueue behavior only.
|
|
86
|
-
mock.module("../../hooks/manager.js", () => ({
|
|
87
|
-
getHookManager: () => ({
|
|
88
|
-
trigger: () => undefined,
|
|
89
|
-
}),
|
|
90
|
-
}));
|
|
91
|
-
|
|
92
86
|
mock.module("../../tools/browser/browser-screencast.js", () => ({
|
|
93
87
|
unregisterConversationSender: () => {},
|
|
94
88
|
}));
|
|
95
89
|
|
|
96
90
|
mock.module("../conversation-notifiers.js", () => ({
|
|
97
91
|
unregisterCallNotifiers: () => {},
|
|
98
|
-
unregisterWatchNotifiers: () => {},
|
|
99
92
|
}));
|
|
100
93
|
|
|
101
94
|
mock.module("../conversation-skill-tools.js", () => ({
|
|
@@ -105,8 +98,7 @@ mock.module("../conversation-skill-tools.js", () => ({
|
|
|
105
98
|
// Dynamic import after mock.module calls so stubs take effect.
|
|
106
99
|
const { disposeConversation } = await import("../conversation-lifecycle.js");
|
|
107
100
|
type DisposeContext = import("../conversation-lifecycle.js").DisposeContext;
|
|
108
|
-
type TrustClass =
|
|
109
|
-
import("../../runtime/actor-trust-resolver.js").TrustClass;
|
|
101
|
+
type TrustClass = import("../../runtime/actor-trust-resolver.js").TrustClass;
|
|
110
102
|
|
|
111
103
|
// ---------------------------------------------------------------------------
|
|
112
104
|
// Fixture builder — minimal DisposeContext satisfying the interface shape.
|