@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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { ContextWindowResult } from "../../../../context/window-manager.js";
|
|
4
|
+
import type { Conversation } from "../../../../daemon/conversation.js";
|
|
5
|
+
import type { Message } from "../../../../providers/types.js";
|
|
6
|
+
import type { RouteContext } from "../../../http-router.js";
|
|
7
|
+
import type { PlaygroundRouteDeps } from "../deps.js";
|
|
8
|
+
import { forceCompactRouteDefinitions } from "../force-compact.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
interface FakeConversationOptions {
|
|
15
|
+
messagesBefore?: Message[];
|
|
16
|
+
messagesAfter?: Message[];
|
|
17
|
+
result?: Partial<ContextWindowResult>;
|
|
18
|
+
processing?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FakeConversation {
|
|
22
|
+
readonly conversation: Conversation;
|
|
23
|
+
readonly forceCompactCallCount: () => number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeFakeConversation(
|
|
27
|
+
options: FakeConversationOptions = {},
|
|
28
|
+
): FakeConversation {
|
|
29
|
+
const messagesBefore = options.messagesBefore ?? [];
|
|
30
|
+
const messagesAfter = options.messagesAfter ?? messagesBefore;
|
|
31
|
+
let calls = 0;
|
|
32
|
+
let returnedAfter = false;
|
|
33
|
+
|
|
34
|
+
const baseResult: ContextWindowResult = {
|
|
35
|
+
messages: messagesAfter,
|
|
36
|
+
compacted: true,
|
|
37
|
+
previousEstimatedInputTokens: 0,
|
|
38
|
+
estimatedInputTokens: 0,
|
|
39
|
+
maxInputTokens: 100_000,
|
|
40
|
+
thresholdTokens: 80_000,
|
|
41
|
+
compactedMessages: 0,
|
|
42
|
+
compactedPersistedMessages: 0,
|
|
43
|
+
summaryCalls: 0,
|
|
44
|
+
summaryInputTokens: 0,
|
|
45
|
+
summaryOutputTokens: 0,
|
|
46
|
+
summaryModel: "",
|
|
47
|
+
summaryText: "",
|
|
48
|
+
...options.result,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const fake = {
|
|
52
|
+
processing: options.processing ?? false,
|
|
53
|
+
getMessages(): Message[] {
|
|
54
|
+
// First call returns the pre-compaction messages; subsequent calls
|
|
55
|
+
// return the post-compaction messages. This mirrors how the route
|
|
56
|
+
// reads the state twice (before/after `forceCompact()`).
|
|
57
|
+
if (!returnedAfter && calls === 0) return messagesBefore;
|
|
58
|
+
return messagesAfter;
|
|
59
|
+
},
|
|
60
|
+
async forceCompact(): Promise<ContextWindowResult> {
|
|
61
|
+
calls += 1;
|
|
62
|
+
returnedAfter = true;
|
|
63
|
+
return baseResult;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
conversation: fake as unknown as Conversation,
|
|
69
|
+
forceCompactCallCount: () => calls,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeDeps(
|
|
74
|
+
overrides: Partial<PlaygroundRouteDeps> = {},
|
|
75
|
+
): PlaygroundRouteDeps {
|
|
76
|
+
return {
|
|
77
|
+
getConversationById: async () => undefined,
|
|
78
|
+
isPlaygroundEnabled: () => true,
|
|
79
|
+
listConversationsByTitlePrefix: () => [],
|
|
80
|
+
deleteConversationById: () => false,
|
|
81
|
+
createConversation: async () => ({ id: "conv-test" }),
|
|
82
|
+
addMessage: async () => ({ id: "msg-test" }),
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeRouteContext(id: string): RouteContext {
|
|
88
|
+
const url = new URL(
|
|
89
|
+
`http://localhost/v1/conversations/${id}/playground/compact`,
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
req: new Request(url, { method: "POST" }),
|
|
93
|
+
url,
|
|
94
|
+
server: {} as RouteContext["server"],
|
|
95
|
+
authContext: {
|
|
96
|
+
subject: "test-user",
|
|
97
|
+
principalType: "local",
|
|
98
|
+
assistantId: "self",
|
|
99
|
+
scopeProfile: "local_v1",
|
|
100
|
+
scopes: new Set(["local.all" as const]),
|
|
101
|
+
policyEpoch: 0,
|
|
102
|
+
},
|
|
103
|
+
params: { id },
|
|
104
|
+
} as unknown as RouteContext;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Tests
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe("forceCompactRouteDefinitions", () => {
|
|
112
|
+
test("exposes a single POST route with the expected endpoint + policy key", () => {
|
|
113
|
+
const routes = forceCompactRouteDefinitions(makeDeps());
|
|
114
|
+
expect(routes).toHaveLength(1);
|
|
115
|
+
expect(routes[0].endpoint).toBe("conversations/:id/playground/compact");
|
|
116
|
+
expect(routes[0].method).toBe("POST");
|
|
117
|
+
expect(routes[0].policyKey).toBe("conversations/playground/compact");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("returns 404 with playground_disabled code when the playground flag is disabled", async () => {
|
|
121
|
+
const deps = makeDeps({ isPlaygroundEnabled: () => false });
|
|
122
|
+
const [route] = forceCompactRouteDefinitions(deps);
|
|
123
|
+
|
|
124
|
+
const res = await route.handler(makeRouteContext("conv-abc"));
|
|
125
|
+
expect(res.status).toBe(404);
|
|
126
|
+
|
|
127
|
+
const body = (await res.json()) as {
|
|
128
|
+
error: { code: string; message: string };
|
|
129
|
+
};
|
|
130
|
+
// Distinct from `conversation_not_found` so the Swift client can
|
|
131
|
+
// surface the right toast text without sniffing the URL path.
|
|
132
|
+
expect(body.error.code).toBe("playground_disabled");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("returns 404 with conversation_not_found code when the conversation is missing", async () => {
|
|
136
|
+
const deps = makeDeps({
|
|
137
|
+
isPlaygroundEnabled: () => true,
|
|
138
|
+
getConversationById: async () => undefined,
|
|
139
|
+
});
|
|
140
|
+
const [route] = forceCompactRouteDefinitions(deps);
|
|
141
|
+
|
|
142
|
+
const res = await route.handler(makeRouteContext("conv-missing"));
|
|
143
|
+
expect(res.status).toBe(404);
|
|
144
|
+
|
|
145
|
+
const body = (await res.json()) as {
|
|
146
|
+
error: { code: string; message: string };
|
|
147
|
+
};
|
|
148
|
+
expect(body.error.code).toBe("conversation_not_found");
|
|
149
|
+
expect(body.error.message).toContain("conv-missing");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("forces compaction and returns before/after tokens + summary metadata", async () => {
|
|
153
|
+
const messagesBefore: Message[] = [
|
|
154
|
+
{ role: "user", content: [{ type: "text", text: "hello world" }] },
|
|
155
|
+
{
|
|
156
|
+
role: "assistant",
|
|
157
|
+
content: [{ type: "text", text: "hi there from the assistant" }],
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
const messagesAfter: Message[] = [
|
|
161
|
+
{
|
|
162
|
+
role: "user",
|
|
163
|
+
content: [{ type: "text", text: "hello" }],
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const fake = makeFakeConversation({
|
|
168
|
+
messagesBefore,
|
|
169
|
+
messagesAfter,
|
|
170
|
+
result: {
|
|
171
|
+
compacted: true,
|
|
172
|
+
summaryText: "one-line summary of the earlier turns",
|
|
173
|
+
compactedPersistedMessages: 7,
|
|
174
|
+
summaryFailed: false,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const deps = makeDeps({
|
|
179
|
+
isPlaygroundEnabled: () => true,
|
|
180
|
+
getConversationById: async () => fake.conversation,
|
|
181
|
+
});
|
|
182
|
+
const [route] = forceCompactRouteDefinitions(deps);
|
|
183
|
+
|
|
184
|
+
const res = await route.handler(makeRouteContext("conv-ok"));
|
|
185
|
+
expect(res.status).toBe(200);
|
|
186
|
+
|
|
187
|
+
const body = (await res.json()) as {
|
|
188
|
+
compacted: boolean;
|
|
189
|
+
previousTokens: number;
|
|
190
|
+
newTokens: number;
|
|
191
|
+
summaryText: string | null;
|
|
192
|
+
messagesRemoved: number;
|
|
193
|
+
summaryFailed: boolean | null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
expect(body.compacted).toBe(true);
|
|
197
|
+
expect(body.summaryText).toBe("one-line summary of the earlier turns");
|
|
198
|
+
expect(body.messagesRemoved).toBe(7);
|
|
199
|
+
expect(body.summaryFailed).toBe(false);
|
|
200
|
+
expect(body.previousTokens).toBeGreaterThan(0);
|
|
201
|
+
expect(body.newTokens).toBeGreaterThan(0);
|
|
202
|
+
// The post-compaction message set is strictly smaller, so the
|
|
203
|
+
// reported token count should fall.
|
|
204
|
+
expect(body.newTokens).toBeLessThan(body.previousTokens);
|
|
205
|
+
|
|
206
|
+
expect(fake.forceCompactCallCount()).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("returns 409 and skips forceCompact when conversation is already processing", async () => {
|
|
210
|
+
// Simulate a turn (or a concurrent /compact) already in flight against
|
|
211
|
+
// this conversation. A second playground POST landing in this window
|
|
212
|
+
// would otherwise race with the first call: duplicate
|
|
213
|
+
// `contextCompactedMessageCount` increments, duplicate
|
|
214
|
+
// `context_compacted` SSE events, and double usage recording. Easy to
|
|
215
|
+
// trigger by double-clicking the playground "Force Compact" button.
|
|
216
|
+
const fake = makeFakeConversation({
|
|
217
|
+
messagesBefore: [
|
|
218
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
219
|
+
],
|
|
220
|
+
processing: true,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const deps = makeDeps({
|
|
224
|
+
isPlaygroundEnabled: () => true,
|
|
225
|
+
getConversationById: async () => fake.conversation,
|
|
226
|
+
});
|
|
227
|
+
const [route] = forceCompactRouteDefinitions(deps);
|
|
228
|
+
|
|
229
|
+
const res = await route.handler(makeRouteContext("conv-busy"));
|
|
230
|
+
expect(res.status).toBe(409);
|
|
231
|
+
|
|
232
|
+
const body = (await res.json()) as {
|
|
233
|
+
error: { code: string; message: string };
|
|
234
|
+
};
|
|
235
|
+
expect(body.error.code).toBe("CONFLICT");
|
|
236
|
+
expect(body.error.message).toContain("already in progress");
|
|
237
|
+
|
|
238
|
+
// Critical: we must NOT have invoked forceCompact a second time while
|
|
239
|
+
// an existing call was in flight.
|
|
240
|
+
expect(fake.forceCompactCallCount()).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("defaults summaryText/summaryFailed to null when forceCompact omits them", async () => {
|
|
244
|
+
const fake = makeFakeConversation({
|
|
245
|
+
messagesBefore: [
|
|
246
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
247
|
+
],
|
|
248
|
+
messagesAfter: [
|
|
249
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
250
|
+
],
|
|
251
|
+
result: {
|
|
252
|
+
compacted: false,
|
|
253
|
+
// Intentionally leave summaryText as "" and summaryFailed undefined
|
|
254
|
+
// so the route's ?? coalescing is exercised.
|
|
255
|
+
summaryText: "",
|
|
256
|
+
summaryFailed: undefined,
|
|
257
|
+
compactedPersistedMessages: 0,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const deps = makeDeps({
|
|
262
|
+
isPlaygroundEnabled: () => true,
|
|
263
|
+
getConversationById: async () => fake.conversation,
|
|
264
|
+
});
|
|
265
|
+
const [route] = forceCompactRouteDefinitions(deps);
|
|
266
|
+
|
|
267
|
+
const res = await route.handler(makeRouteContext("conv-noop"));
|
|
268
|
+
expect(res.status).toBe(200);
|
|
269
|
+
|
|
270
|
+
const body = (await res.json()) as {
|
|
271
|
+
compacted: boolean;
|
|
272
|
+
summaryText: string | null;
|
|
273
|
+
messagesRemoved: number;
|
|
274
|
+
summaryFailed: boolean | null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
expect(body.compacted).toBe(false);
|
|
278
|
+
// summaryText is "" (falsy) so `??` keeps it as "" — not null. We only
|
|
279
|
+
// substitute null when the field is nullish, matching the handler.
|
|
280
|
+
expect(body.summaryText).toBe("");
|
|
281
|
+
expect(body.summaryFailed).toBeNull();
|
|
282
|
+
expect(body.messagesRemoved).toBe(0);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { Conversation } from "../../../../daemon/conversation.js";
|
|
4
|
+
import type { PlaygroundRouteDeps } from "../deps.js";
|
|
5
|
+
import { assertPlaygroundEnabled } from "../guard.js";
|
|
6
|
+
import { playgroundRouteDefinitions } from "../index.js";
|
|
7
|
+
|
|
8
|
+
function makeDeps(enabled: boolean): PlaygroundRouteDeps {
|
|
9
|
+
return {
|
|
10
|
+
getConversationById: async (
|
|
11
|
+
_id: string,
|
|
12
|
+
): Promise<Conversation | undefined> => undefined,
|
|
13
|
+
isPlaygroundEnabled: () => enabled,
|
|
14
|
+
listConversationsByTitlePrefix: () => [],
|
|
15
|
+
deleteConversationById: () => false,
|
|
16
|
+
createConversation: async (_title: string) => ({ id: "conv-test" }),
|
|
17
|
+
addMessage: async (
|
|
18
|
+
_conversationId: string,
|
|
19
|
+
_role: "user" | "assistant",
|
|
20
|
+
_contentJson: string,
|
|
21
|
+
) => ({ id: "msg-test" }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("assertPlaygroundEnabled", () => {
|
|
26
|
+
test("returns a 404 Response when the flag is disabled", async () => {
|
|
27
|
+
const result = assertPlaygroundEnabled(makeDeps(false));
|
|
28
|
+
|
|
29
|
+
expect(result).toBeInstanceOf(Response);
|
|
30
|
+
expect(result?.status).toBe(404);
|
|
31
|
+
|
|
32
|
+
const body = (await result?.json()) as {
|
|
33
|
+
error: { code: string; message: string };
|
|
34
|
+
};
|
|
35
|
+
// The body code must be `playground_disabled` (not the generic
|
|
36
|
+
// `NOT_FOUND`) so the Swift `CompactionPlaygroundClient` can route
|
|
37
|
+
// this to `.notAvailable` rather than `.notFound`. The two cases
|
|
38
|
+
// collide on conv-scoped routes because this guard runs *before*
|
|
39
|
+
// the conversation lookup — the URL alone cannot tell them apart.
|
|
40
|
+
expect(body.error.code).toBe("playground_disabled");
|
|
41
|
+
expect(body.error.message).toBe("Compaction playground is not enabled");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns null when the flag is enabled", () => {
|
|
45
|
+
expect(assertPlaygroundEnabled(makeDeps(true))).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("playgroundRouteDefinitions", () => {
|
|
50
|
+
test("returns route definitions regardless of flag state (guard runs per-request)", () => {
|
|
51
|
+
// The flag check happens inside each route's handler via
|
|
52
|
+
// `assertPlaygroundEnabled`, not at registration time. The aggregator
|
|
53
|
+
// always returns every registered route; each handler returns 404 when
|
|
54
|
+
// the flag is disabled.
|
|
55
|
+
expect(playgroundRouteDefinitions(makeDeps(true)).length).toBeGreaterThan(
|
|
56
|
+
0,
|
|
57
|
+
);
|
|
58
|
+
expect(playgroundRouteDefinitions(makeDeps(false)).length).toBeGreaterThan(
|
|
59
|
+
0,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("registers the inject-failures playground route", () => {
|
|
64
|
+
const routes = playgroundRouteDefinitions(makeDeps(true));
|
|
65
|
+
expect(
|
|
66
|
+
routes.some(
|
|
67
|
+
(r) =>
|
|
68
|
+
r.endpoint ===
|
|
69
|
+
"conversations/:id/playground/inject-compaction-failures" &&
|
|
70
|
+
r.method === "POST",
|
|
71
|
+
),
|
|
72
|
+
).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("registers the seed-conversation endpoint", () => {
|
|
76
|
+
const routes = playgroundRouteDefinitions(makeDeps(true));
|
|
77
|
+
const endpoints = routes.map((r) => `${r.method} ${r.endpoint}`);
|
|
78
|
+
expect(endpoints).toContain("POST playground/seed-conversation");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the inject-compaction-failures playground endpoint.
|
|
3
|
+
*
|
|
4
|
+
* This endpoint is dev-only (gated by the `compaction-playground` feature
|
|
5
|
+
* flag) and directly mutates `consecutiveCompactionFailures` and/or
|
|
6
|
+
* `compactionCircuitOpenUntil` on a live `Conversation`. It is used by the
|
|
7
|
+
* macOS playground UI and integration tests to drive the circuit breaker
|
|
8
|
+
* into interesting states without having to wait for three real summary
|
|
9
|
+
* LLM failures.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { Conversation } from "../../../../daemon/conversation.js";
|
|
14
|
+
import type { ServerMessage } from "../../../../daemon/message-protocol.js";
|
|
15
|
+
import type { RouteContext } from "../../../http-router.js";
|
|
16
|
+
import type { PlaygroundRouteDeps } from "../deps.js";
|
|
17
|
+
import { injectFailuresRouteDefinitions } from "../inject-failures.js";
|
|
18
|
+
|
|
19
|
+
interface MockConversation {
|
|
20
|
+
readonly conversationId: string;
|
|
21
|
+
consecutiveCompactionFailures: number;
|
|
22
|
+
compactionCircuitOpenUntil: number | null;
|
|
23
|
+
contextCompactedMessageCount: number;
|
|
24
|
+
contextCompactedAt: number | null;
|
|
25
|
+
sentMessages: ServerMessage[];
|
|
26
|
+
sendToClient: (msg: ServerMessage) => void;
|
|
27
|
+
getMessages: () => unknown[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeConversation(id = "conv-playground-test"): MockConversation {
|
|
31
|
+
const sentMessages: ServerMessage[] = [];
|
|
32
|
+
return {
|
|
33
|
+
conversationId: id,
|
|
34
|
+
consecutiveCompactionFailures: 0,
|
|
35
|
+
compactionCircuitOpenUntil: null,
|
|
36
|
+
contextCompactedMessageCount: 0,
|
|
37
|
+
contextCompactedAt: null,
|
|
38
|
+
sentMessages,
|
|
39
|
+
sendToClient: (msg) => sentMessages.push(msg),
|
|
40
|
+
getMessages: () => [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeDeps(
|
|
45
|
+
opts: {
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
conversation?: MockConversation | undefined;
|
|
48
|
+
} = {},
|
|
49
|
+
): PlaygroundRouteDeps {
|
|
50
|
+
const enabled = opts.enabled ?? true;
|
|
51
|
+
const conversation = opts.conversation;
|
|
52
|
+
return {
|
|
53
|
+
isPlaygroundEnabled: () => enabled,
|
|
54
|
+
getConversationById: async (id) => {
|
|
55
|
+
if (!conversation) return undefined;
|
|
56
|
+
if (conversation.conversationId !== id) return undefined;
|
|
57
|
+
return conversation as unknown as Conversation;
|
|
58
|
+
},
|
|
59
|
+
listConversationsByTitlePrefix: () => [],
|
|
60
|
+
deleteConversationById: () => false,
|
|
61
|
+
createConversation: async () => ({ id: "conv-test" }),
|
|
62
|
+
addMessage: async () => ({ id: "msg-test" }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getInjectRoute(deps: PlaygroundRouteDeps) {
|
|
67
|
+
const routes = injectFailuresRouteDefinitions(deps);
|
|
68
|
+
const route = routes.find(
|
|
69
|
+
(r) =>
|
|
70
|
+
r.endpoint ===
|
|
71
|
+
"conversations/:id/playground/inject-compaction-failures" &&
|
|
72
|
+
r.method === "POST",
|
|
73
|
+
);
|
|
74
|
+
if (!route) {
|
|
75
|
+
throw new Error("inject-failures route not registered");
|
|
76
|
+
}
|
|
77
|
+
return route;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function invoke(
|
|
81
|
+
route: ReturnType<typeof getInjectRoute>,
|
|
82
|
+
conversationId: string,
|
|
83
|
+
body: unknown,
|
|
84
|
+
): Promise<Response> {
|
|
85
|
+
const url = `http://localhost/v1/conversations/${conversationId}/playground/inject-compaction-failures`;
|
|
86
|
+
const req = new Request(url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json" },
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
});
|
|
91
|
+
return Promise.resolve(
|
|
92
|
+
route.handler({
|
|
93
|
+
req,
|
|
94
|
+
url: new URL(url),
|
|
95
|
+
params: { id: conversationId },
|
|
96
|
+
} as unknown as RouteContext),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("POST /v1/conversations/:id/playground/inject-compaction-failures", () => {
|
|
101
|
+
test("returns 404 with playground_disabled code when the compaction-playground flag is disabled", async () => {
|
|
102
|
+
const conversation = makeConversation();
|
|
103
|
+
const deps = makeDeps({ enabled: false, conversation });
|
|
104
|
+
const route = getInjectRoute(deps);
|
|
105
|
+
|
|
106
|
+
const res = await invoke(route, conversation.conversationId, {});
|
|
107
|
+
expect(res.status).toBe(404);
|
|
108
|
+
|
|
109
|
+
const body = (await res.json()) as {
|
|
110
|
+
error: { code: string; message: string };
|
|
111
|
+
};
|
|
112
|
+
// Distinct from `conversation_not_found` so the Swift client can
|
|
113
|
+
// surface the right toast text without sniffing the URL path.
|
|
114
|
+
expect(body.error.code).toBe("playground_disabled");
|
|
115
|
+
|
|
116
|
+
// Flag-gated — the handler must not mutate conversation state or emit
|
|
117
|
+
// events when the playground is disabled.
|
|
118
|
+
expect(conversation.sentMessages).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("returns 404 with conversation_not_found code when the conversation is missing", async () => {
|
|
122
|
+
const deps = makeDeps({ enabled: true, conversation: undefined });
|
|
123
|
+
const route = getInjectRoute(deps);
|
|
124
|
+
|
|
125
|
+
const res = await invoke(route, "missing-conv-id", {});
|
|
126
|
+
expect(res.status).toBe(404);
|
|
127
|
+
|
|
128
|
+
const body = (await res.json()) as {
|
|
129
|
+
error: { code: string; message: string };
|
|
130
|
+
};
|
|
131
|
+
expect(body.error.code).toBe("conversation_not_found");
|
|
132
|
+
expect(body.error.message).toContain("missing-conv-id");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("mutates both fields and emits compaction_circuit_open when both provided", async () => {
|
|
136
|
+
const conversation = makeConversation("conv-open");
|
|
137
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
138
|
+
const route = getInjectRoute(deps);
|
|
139
|
+
|
|
140
|
+
const beforeNow = Date.now();
|
|
141
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
142
|
+
consecutiveFailures: 3,
|
|
143
|
+
circuitOpenForMs: 60_000,
|
|
144
|
+
});
|
|
145
|
+
const afterNow = Date.now();
|
|
146
|
+
expect(res.status).toBe(200);
|
|
147
|
+
|
|
148
|
+
expect(conversation.consecutiveCompactionFailures).toBe(3);
|
|
149
|
+
expect(conversation.compactionCircuitOpenUntil).not.toBeNull();
|
|
150
|
+
const openUntil = conversation.compactionCircuitOpenUntil!;
|
|
151
|
+
expect(openUntil).toBeGreaterThanOrEqual(beforeNow + 60_000);
|
|
152
|
+
expect(openUntil).toBeLessThanOrEqual(afterNow + 60_000);
|
|
153
|
+
|
|
154
|
+
// Exactly one event emitted with the expected shape.
|
|
155
|
+
expect(conversation.sentMessages).toHaveLength(1);
|
|
156
|
+
expect(conversation.sentMessages[0]).toEqual({
|
|
157
|
+
type: "compaction_circuit_open",
|
|
158
|
+
conversationId: conversation.conversationId,
|
|
159
|
+
reason: "3_consecutive_failures",
|
|
160
|
+
openUntil,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("clears the circuit and emits compaction_circuit_closed on circuitOpenForMs: 0", async () => {
|
|
165
|
+
const conversation = makeConversation("conv-close");
|
|
166
|
+
// Start with an open breaker so the endpoint can clear it.
|
|
167
|
+
conversation.compactionCircuitOpenUntil = Date.now() + 10_000;
|
|
168
|
+
conversation.consecutiveCompactionFailures = 3;
|
|
169
|
+
|
|
170
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
171
|
+
const route = getInjectRoute(deps);
|
|
172
|
+
|
|
173
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
174
|
+
circuitOpenForMs: 0,
|
|
175
|
+
});
|
|
176
|
+
expect(res.status).toBe(200);
|
|
177
|
+
|
|
178
|
+
expect(conversation.compactionCircuitOpenUntil).toBeNull();
|
|
179
|
+
// consecutiveFailures was not specified in the body, so it must be
|
|
180
|
+
// unchanged (the endpoint only mutates fields that are explicitly set).
|
|
181
|
+
expect(conversation.consecutiveCompactionFailures).toBe(3);
|
|
182
|
+
|
|
183
|
+
expect(conversation.sentMessages).toHaveLength(1);
|
|
184
|
+
expect(conversation.sentMessages[0]).toEqual({
|
|
185
|
+
type: "compaction_circuit_closed",
|
|
186
|
+
conversationId: conversation.conversationId,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("is a no-op on the event channel when circuitOpenForMs: 0 but the breaker is already closed", async () => {
|
|
191
|
+
const conversation = makeConversation("conv-already-closed");
|
|
192
|
+
// Breaker is already closed before the request.
|
|
193
|
+
expect(conversation.compactionCircuitOpenUntil).toBeNull();
|
|
194
|
+
|
|
195
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
196
|
+
const route = getInjectRoute(deps);
|
|
197
|
+
|
|
198
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
199
|
+
circuitOpenForMs: 0,
|
|
200
|
+
});
|
|
201
|
+
expect(res.status).toBe(200);
|
|
202
|
+
|
|
203
|
+
// Still null after the request.
|
|
204
|
+
expect(conversation.compactionCircuitOpenUntil).toBeNull();
|
|
205
|
+
// Critically: no `compaction_circuit_closed` event is emitted, since
|
|
206
|
+
// there was no open→closed transition. Clients must not see a spurious
|
|
207
|
+
// close event.
|
|
208
|
+
expect(conversation.sentMessages).toHaveLength(0);
|
|
209
|
+
|
|
210
|
+
// Response body still reflects the expected shape.
|
|
211
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
212
|
+
expect(body.compactionCircuitOpenUntil).toBeNull();
|
|
213
|
+
expect(body.isCircuitOpen).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("rejects out-of-range consecutiveFailures with 400", async () => {
|
|
217
|
+
const conversation = makeConversation();
|
|
218
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
219
|
+
const route = getInjectRoute(deps);
|
|
220
|
+
|
|
221
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
222
|
+
consecutiveFailures: 99, // above max (10)
|
|
223
|
+
});
|
|
224
|
+
expect(res.status).toBe(400);
|
|
225
|
+
|
|
226
|
+
// No mutation, no event.
|
|
227
|
+
expect(conversation.consecutiveCompactionFailures).toBe(0);
|
|
228
|
+
expect(conversation.sentMessages).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("rejects out-of-range circuitOpenForMs with 400", async () => {
|
|
232
|
+
const conversation = makeConversation();
|
|
233
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
234
|
+
const route = getInjectRoute(deps);
|
|
235
|
+
|
|
236
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
237
|
+
circuitOpenForMs: 25 * 60 * 60 * 1000, // 25h, above the 24h cap
|
|
238
|
+
});
|
|
239
|
+
expect(res.status).toBe(400);
|
|
240
|
+
|
|
241
|
+
expect(conversation.compactionCircuitOpenUntil).toBeNull();
|
|
242
|
+
expect(conversation.sentMessages).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("rejects negative consecutiveFailures with 400", async () => {
|
|
246
|
+
const conversation = makeConversation();
|
|
247
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
248
|
+
const route = getInjectRoute(deps);
|
|
249
|
+
|
|
250
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
251
|
+
consecutiveFailures: -1,
|
|
252
|
+
});
|
|
253
|
+
expect(res.status).toBe(400);
|
|
254
|
+
expect(conversation.consecutiveCompactionFailures).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("response body includes the full CompactionStateResponse shape", async () => {
|
|
258
|
+
const conversation = makeConversation("conv-shape");
|
|
259
|
+
const deps = makeDeps({ enabled: true, conversation });
|
|
260
|
+
const route = getInjectRoute(deps);
|
|
261
|
+
|
|
262
|
+
const res = await invoke(route, conversation.conversationId, {
|
|
263
|
+
consecutiveFailures: 2,
|
|
264
|
+
});
|
|
265
|
+
expect(res.status).toBe(200);
|
|
266
|
+
|
|
267
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
268
|
+
const requiredKeys = [
|
|
269
|
+
"estimatedInputTokens",
|
|
270
|
+
"maxInputTokens",
|
|
271
|
+
"compactThresholdRatio",
|
|
272
|
+
"thresholdTokens",
|
|
273
|
+
"messageCount",
|
|
274
|
+
"contextCompactedMessageCount",
|
|
275
|
+
"contextCompactedAt",
|
|
276
|
+
"consecutiveCompactionFailures",
|
|
277
|
+
"compactionCircuitOpenUntil",
|
|
278
|
+
"isCircuitOpen",
|
|
279
|
+
"isCompactionEnabled",
|
|
280
|
+
];
|
|
281
|
+
for (const key of requiredKeys) {
|
|
282
|
+
expect(body).toHaveProperty(key);
|
|
283
|
+
}
|
|
284
|
+
expect(body.consecutiveCompactionFailures).toBe(2);
|
|
285
|
+
expect(body.isCircuitOpen).toBe(false);
|
|
286
|
+
expect(body.compactionCircuitOpenUntil).toBeNull();
|
|
287
|
+
expect(typeof body.estimatedInputTokens).toBe("number");
|
|
288
|
+
expect(typeof body.maxInputTokens).toBe("number");
|
|
289
|
+
expect(typeof body.compactThresholdRatio).toBe("number");
|
|
290
|
+
expect(typeof body.thresholdTokens).toBe("number");
|
|
291
|
+
expect(typeof body.messageCount).toBe("number");
|
|
292
|
+
expect(typeof body.isCompactionEnabled).toBe("boolean");
|
|
293
|
+
});
|
|
294
|
+
});
|