@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea
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/Dockerfile +20 -4
- package/bun.lock +2 -2
- package/docker-entrypoint.sh +4 -2
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/examples/plugins/echo/README.md +61 -66
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +3378 -335
- package/package.json +2 -2
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +37 -87
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -3
- package/src/__tests__/anthropic-provider.test.ts +95 -2
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
- package/src/__tests__/btw-routes.test.ts +62 -3
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- package/src/__tests__/catalog-files.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/clawhub-files.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction.benchmark.test.ts +0 -30
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
- package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-history-web-search.test.ts +11 -1
- package/src/__tests__/conversation-pairing.test.ts +4 -31
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
- package/src/__tests__/conversation-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
- package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
- package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
- package/src/__tests__/conversation-sync-tags.test.ts +27 -15
- package/src/__tests__/conversation-title-service.test.ts +135 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
- package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -0
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -0
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/identity-intro-cache.test.ts +12 -100
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
- package/src/__tests__/injector-background-turn.test.ts +3 -9
- package/src/__tests__/injector-chain.test.ts +139 -275
- package/src/__tests__/injector-disk-pressure.test.ts +75 -41
- package/src/__tests__/injector-document-comments.test.ts +3 -3
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +31 -37
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/list-messages-page-latest.test.ts +60 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
- package/src/__tests__/llm-usage-store.test.ts +223 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- package/src/__tests__/native-web-search.test.ts +191 -0
- package/src/__tests__/onboarding-template-contract.test.ts +2 -0
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +31 -1
- package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-api-shim.test.ts +3 -6
- package/src/__tests__/plugin-bootstrap.test.ts +14 -40
- package/src/__tests__/plugin-registry.test.ts +3 -76
- package/src/__tests__/plugin-types.test.ts +0 -193
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
- package/src/__tests__/schedule-routes.test.ts +603 -2
- package/src/__tests__/schedule-store.test.ts +41 -0
- package/src/__tests__/schedule-tools.test.ts +35 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/server-history-render.test.ts +314 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/skillssh-files.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +20 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +6 -1
- package/src/__tests__/title-generate-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -2
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +196 -0
- package/src/acp/auto-install.ts +177 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +83 -29
- package/src/acp/resolve-agent.test.ts +320 -7
- package/src/acp/resolve-agent.ts +182 -18
- package/src/acp/resume-hint.ts +25 -0
- package/src/acp/session-manager.ts +495 -73
- package/src/acp/types.ts +8 -0
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +362 -485
- package/src/api/events/assistant-thinking-delta.ts +33 -0
- package/src/api/events/tool-output-chunk.ts +45 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/trace-event.ts +69 -0
- package/src/api/index.ts +48 -13
- package/src/api/responses/conversation-message.ts +374 -0
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/assistant-feature-flags.ts +22 -11
- package/src/config/bundled-skills/acp/SKILL.md +83 -31
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -398
- package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -7
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +48 -7
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-v2.ts +8 -0
- package/src/config/schemas/memory-v3.ts +8 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +1 -1
- package/src/context/strip-injections.ts +128 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +5 -7
- package/src/credential-execution/executable-discovery.ts +16 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
- package/src/daemon/conversation-agent-loop.ts +301 -997
- package/src/daemon/conversation-history.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +3 -4
- package/src/daemon/conversation-messaging.ts +7 -6
- package/src/daemon/conversation-process.ts +11 -16
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +218 -398
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +32 -20
- package/src/daemon/external-plugins-bootstrap.ts +17 -18
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +42 -10
- package/src/daemon/lifecycle.ts +25 -0
- package/src/daemon/message-types/apps.ts +1 -29
- package/src/daemon/message-types/messages.ts +9 -57
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +136 -3
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/overflow-reduction-loop.ts +230 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- package/src/daemon/server.ts +2 -0
- package/src/daemon/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +19 -0
- package/src/daemon/wake-target-adapter.ts +3 -1
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/home/home-greeting-cache.ts +24 -1
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
- package/src/memory/auth-fallback-events-store.ts +94 -0
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/memory/graph/conversation-graph-memory.ts +65 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +32 -5
- package/src/memory/llm-usage-store.ts +224 -50
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
- package/src/memory/v2/consolidation-job.ts +14 -5
- package/src/notifications/conversation-pairing.ts +8 -15
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/home-feed-side-effect.ts +12 -1
- package/src/permissions/prompter.ts +4 -0
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +7 -5
- package/src/plugin-api/types.ts +151 -1
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/plugins/defaults/compaction/package.json +1 -1
- package/src/plugins/defaults/compaction/register.ts +8 -19
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/register.ts +8 -13
- package/src/plugins/defaults/index.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +16 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +48 -12
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +104 -32
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +96 -30
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
- package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
- package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +51 -45
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +61 -46
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/title-generate/package.json +1 -1
- package/src/plugins/defaults/title-generate/register.ts +18 -18
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +1 -1
- package/src/plugins/defaults/tool-error/register.ts +9 -21
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
- package/src/plugins/external-api.ts +2 -2
- package/src/plugins/pipeline.ts +6 -305
- package/src/plugins/registry.ts +10 -55
- package/src/plugins/types.ts +62 -797
- package/src/plugins/user-loader.ts +30 -127
- package/src/proactive-artifact/aux-message-injector.ts +4 -4
- package/src/proactive-artifact/job.test.ts +8 -13
- package/src/prompts/__tests__/system-prompt.test.ts +42 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +15 -0
- package/src/providers/anthropic/client.ts +37 -29
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -0
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/placeholder-sentinels.ts +35 -0
- package/src/runtime/__tests__/agent-wake.test.ts +10 -6
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +2 -5
- package/src/runtime/assistant-event-hub.ts +37 -7
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +3 -1
- package/src/runtime/routes/conversation-routes.ts +372 -185
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/documents-routes.ts +4 -0
- package/src/runtime/routes/domain-routes.ts +51 -37
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +28 -34
- package/src/runtime/routes/gateway-log-routes.ts +26 -4
- package/src/runtime/routes/heartbeat-routes.ts +32 -12
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +224 -18
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/a2a.ts +12 -10
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +4 -0
- package/src/runtime/routes/integrations/slack/share.ts +27 -6
- package/src/runtime/routes/integrations/telegram.ts +6 -0
- package/src/runtime/routes/integrations/twilio.ts +42 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
- package/src/runtime/routes/log-export-routes.ts +8 -0
- package/src/runtime/routes/memory-v2-routes.ts +15 -8
- package/src/runtime/routes/memory-v3-routes.ts +66 -34
- package/src/runtime/routes/oauth-apps.ts +66 -12
- package/src/runtime/routes/oauth-providers.ts +44 -5
- package/src/runtime/routes/platform-routes.ts +81 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/schedule-routes.ts +152 -42
- package/src/runtime/routes/secret-routes.ts +14 -2
- package/src/runtime/routes/skills-routes.ts +43 -14
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trust-rules-routes.ts +26 -2
- package/src/runtime/routes/tts-routes.ts +35 -0
- package/src/runtime/routes/types.ts +66 -8
- package/src/runtime/routes/usage-routes.ts +47 -39
- package/src/runtime/routes/webhook-routes.ts +41 -2
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +4 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
- package/src/runtime/services/analyze-conversation.ts +2 -2
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- package/src/signals/cancel.ts +2 -4
- package/src/skills/catalog-files.ts +2 -2
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -2
- package/src/skills/skillssh-files.ts +1 -2
- package/src/subagent/manager.ts +17 -5
- package/src/telemetry/types.ts +29 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
- package/src/telemetry/usage-telemetry-reporter.ts +57 -2
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +7 -1
- package/src/tools/acp/spawn.test.ts +158 -55
- package/src/tools/acp/spawn.ts +47 -72
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +13 -8
- package/src/tools/executor.ts +1 -53
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +11 -3
- package/src/tools/network/web-search-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +207 -48
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +34 -5
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- package/src/util/mutex.ts +47 -0
- package/src/workspace/git-service.ts +1 -42
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/docs/plugins.md +0 -836
- package/examples/plugins/echo/register.ts +0 -184
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/empty-response-pipeline.test.ts +0 -423
- package/src/__tests__/llm-call-pipeline.test.ts +0 -287
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -503
- package/src/__tests__/pipeline-runner.test.ts +0 -564
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
- package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- package/src/home/feature-gate.ts +0 -22
- package/src/memory/v3/provider-blocks.ts +0 -16
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
- package/src/plugins/defaults/empty-response/terminal.ts +0 -106
- package/src/plugins/defaults/injectors/package.json +0 -15
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
- package/src/plugins/defaults/llm-call/register.ts +0 -45
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
- package/src/plugins/defaults/memory-retrieval/package.json +0 -15
- package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
- package/src/plugins/defaults/persistence/package.json +0 -15
- package/src/plugins/defaults/persistence/register.ts +0 -38
- package/src/plugins/defaults/persistence/terminal.ts +0 -83
- package/src/plugins/defaults/title-generate/terminal.ts +0 -31
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
- package/src/plugins/defaults/token-estimate/package.json +0 -15
- package/src/plugins/defaults/token-estimate/register.ts +0 -34
- package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
- package/src/plugins/defaults/tool-error/terminal.ts +0 -47
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
- package/src/plugins/defaults/tool-execute/package.json +0 -15
- package/src/plugins/defaults/tool-execute/register.ts +0 -49
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
- package/src/skills/category-inference.ts +0 -111
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -167,6 +167,7 @@ const mockFailStalledJobs = mock(() => 0);
|
|
|
167
167
|
const mockClaimMemoryJobs = mock(() => []);
|
|
168
168
|
mock.module("../memory/jobs-store.js", () => ({
|
|
169
169
|
claimMemoryJobs: mockClaimMemoryJobs,
|
|
170
|
+
cancelPendingAutomaticConsolidationJobs: mock(() => 0),
|
|
170
171
|
completeMemoryJob: mock(() => {}),
|
|
171
172
|
deferMemoryJob: mock(() => "deferred"),
|
|
172
173
|
EMBED_JOB_TYPES: [],
|
|
@@ -178,7 +179,12 @@ mock.module("../memory/jobs-store.js", () => ({
|
|
|
178
179
|
failStalledJobs: mockFailStalledJobs,
|
|
179
180
|
getMemoryJobCounts: mock(() => ({})),
|
|
180
181
|
hasActiveJobOfType: mock(() => false),
|
|
182
|
+
isAutomaticConsolidationJob: mock(() => true),
|
|
181
183
|
isMemoryEnabled: () => true,
|
|
184
|
+
MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS: {
|
|
185
|
+
automatic: "automatic",
|
|
186
|
+
manual: "manual",
|
|
187
|
+
},
|
|
182
188
|
resetRunningJobsToPending: mock(() => 0),
|
|
183
189
|
SLOW_LLM_JOB_TYPES: [],
|
|
184
190
|
upsertAutoAnalysisJob: mock(() => "job-auto-analysis"),
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* and no session.processing mutation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
12
|
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
12
14
|
// Mocks — must be defined before importing the module under test
|
|
@@ -76,7 +78,13 @@ mock.module("../runtime/routes/identity-intro-cache.js", () => ({
|
|
|
76
78
|
getCachedIntro: () => null,
|
|
77
79
|
readWorkspaceIdentityIntro: () => null,
|
|
78
80
|
setCachedIntro: () => {},
|
|
79
|
-
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const assistantFeatureFlags: Record<string, boolean> = {};
|
|
84
|
+
|
|
85
|
+
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
86
|
+
isAssistantFeatureFlagEnabled: (key: string) =>
|
|
87
|
+
assistantFeatureFlags[key] ?? false,
|
|
80
88
|
}));
|
|
81
89
|
|
|
82
90
|
// Mock getOrCreateConversation from conversation-store so the handler
|
|
@@ -123,6 +131,7 @@ import {
|
|
|
123
131
|
ServiceUnavailableError,
|
|
124
132
|
} from "../runtime/routes/errors.js";
|
|
125
133
|
import type { RouteHandlerArgs } from "../runtime/routes/types.js";
|
|
134
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
126
135
|
|
|
127
136
|
// ---------------------------------------------------------------------------
|
|
128
137
|
// Helpers
|
|
@@ -177,6 +186,12 @@ function makeMockSession(
|
|
|
177
186
|
const route = ROUTES.find((r) => r.endpoint === "btw" && r.method === "POST");
|
|
178
187
|
if (!route) throw new Error("btw route not found in ROUTES");
|
|
179
188
|
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
for (const key of Object.keys(assistantFeatureFlags)) {
|
|
191
|
+
delete assistantFeatureFlags[key];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
180
195
|
async function callHandler(
|
|
181
196
|
body: Record<string, unknown>,
|
|
182
197
|
): Promise<{ result: unknown; error?: unknown }> {
|
|
@@ -324,7 +339,25 @@ describe("POST /v1/btw", () => {
|
|
|
324
339
|
expect(options!.config!.modelIntent).toBeUndefined();
|
|
325
340
|
});
|
|
326
341
|
|
|
327
|
-
test("greeting requests
|
|
342
|
+
test("greeting requests return static fallback when the dynamic greetings flag is off", async () => {
|
|
343
|
+
mockGetOrCreateConversation.mockClear();
|
|
344
|
+
|
|
345
|
+
const { result } = await callHandler({
|
|
346
|
+
conversationKey: "greeting",
|
|
347
|
+
content: "Generate a greeting",
|
|
348
|
+
});
|
|
349
|
+
const text = await readStream(result as ReadableStream<Uint8Array>);
|
|
350
|
+
|
|
351
|
+
expect(text).toContain(
|
|
352
|
+
`event: btw_text_delta\ndata: {"text":"What are we working on?"}`,
|
|
353
|
+
);
|
|
354
|
+
expect(text).toContain("event: btw_complete\ndata: {}");
|
|
355
|
+
expect(mockGetOrCreateConversation).not.toHaveBeenCalled();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("greeting requests pass callSite: 'emptyStateGreeting' when the dynamic greetings flag is on", async () => {
|
|
359
|
+
assistantFeatureFlags["empty-state-dynamic-greetings"] = true;
|
|
360
|
+
|
|
328
361
|
const provider = makeMockProvider();
|
|
329
362
|
const session = makeMockSession(provider);
|
|
330
363
|
mockGetOrCreateConversation.mockImplementationOnce(async () => session);
|
|
@@ -356,6 +389,32 @@ describe("POST /v1/btw", () => {
|
|
|
356
389
|
expect(options!.config!.callSite).toBe("identityIntro");
|
|
357
390
|
});
|
|
358
391
|
|
|
392
|
+
test("identity intro requests do not synthesize a static name greeting", async () => {
|
|
393
|
+
const identityPath = join(getWorkspaceDir(), "IDENTITY.md");
|
|
394
|
+
writeFileSync(
|
|
395
|
+
identityPath,
|
|
396
|
+
"# Identity\n\n- **Name:** Example Assistant\n",
|
|
397
|
+
"utf-8",
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const provider = makeMockProvider();
|
|
402
|
+
const session = makeMockSession(provider);
|
|
403
|
+
mockGetOrCreateConversation.mockImplementationOnce(async () => session);
|
|
404
|
+
|
|
405
|
+
const { result } = await callHandler({
|
|
406
|
+
conversationKey: "identity-intro",
|
|
407
|
+
content: "Generate an intro",
|
|
408
|
+
});
|
|
409
|
+
const text = await readStream(result as ReadableStream<Uint8Array>);
|
|
410
|
+
|
|
411
|
+
expect(provider.sendMessage).toHaveBeenCalledTimes(1);
|
|
412
|
+
expect(text).not.toContain("Hi, I'm Example Assistant!");
|
|
413
|
+
} finally {
|
|
414
|
+
rmSync(identityPath, { force: true });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
359
418
|
// -- No persistence --
|
|
360
419
|
|
|
361
420
|
test("does not persist any messages (addMessage never called)", async () => {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `buildPersistedAssistantContent` stamping native web-search
|
|
3
|
+
* activity (`_activityMetadata`) onto `server_tool_use` blocks at persist time.
|
|
4
|
+
*
|
|
5
|
+
* Native Anthropic web_search resolves mid-stream — `server_tool_complete`
|
|
6
|
+
* fires before `message_complete` — so the captured activity is available when
|
|
7
|
+
* the content is persisted. Unlike external provider tools, a pure-native turn
|
|
8
|
+
* has no `tool_result` and never runs `annotatePersistedAssistantMessage`, so
|
|
9
|
+
* stamping must happen here or the WebSearchProgressCard is lost on a history
|
|
10
|
+
* reopen. Read-side coverage lives in server-history-render.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
// ── Mock platform (must precede imports that read it) ─────────────────────────
|
|
15
|
+
mock.module("../util/logger.js", () => ({
|
|
16
|
+
getLogger: () =>
|
|
17
|
+
new Proxy({} as Record<string, unknown>, {
|
|
18
|
+
get: () => () => {},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module("../config/loader.js", () => ({
|
|
23
|
+
getConfig: () => ({
|
|
24
|
+
skills: {
|
|
25
|
+
entries: {},
|
|
26
|
+
load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
|
|
27
|
+
install: { nodeManager: "npm" },
|
|
28
|
+
allowBundled: null,
|
|
29
|
+
remoteProviders: {
|
|
30
|
+
skillssh: { enabled: true },
|
|
31
|
+
clawhub: { enabled: true },
|
|
32
|
+
},
|
|
33
|
+
remotePolicy: {
|
|
34
|
+
blockSuspicious: true,
|
|
35
|
+
blockMalware: true,
|
|
36
|
+
maxSkillsShRisk: "medium",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
loadConfig: () => ({}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
44
|
+
addMessage: () => ({ id: "mock-msg-id" }),
|
|
45
|
+
getMessageById: () => null,
|
|
46
|
+
updateMessageContent: () => {},
|
|
47
|
+
provenanceFromTrustContext: () => ({}),
|
|
48
|
+
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
mock.module("../memory/llm-request-log-store.js", () => ({
|
|
52
|
+
recordRequestLog: () => {},
|
|
53
|
+
backfillMessageIdOnLogs: () => {},
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
|
57
|
+
import { buildPersistedAssistantContent } from "../daemon/conversation-agent-loop-handlers.js";
|
|
58
|
+
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
59
|
+
import type { ContentBlock } from "../providers/types.js";
|
|
60
|
+
|
|
61
|
+
const webSearchActivity: ToolActivityMetadata = {
|
|
62
|
+
webSearch: {
|
|
63
|
+
query: "vellum docs",
|
|
64
|
+
provider: "anthropic-native",
|
|
65
|
+
resultCount: 1,
|
|
66
|
+
durationMs: 88,
|
|
67
|
+
results: [
|
|
68
|
+
{
|
|
69
|
+
rank: 1,
|
|
70
|
+
title: "Vellum",
|
|
71
|
+
url: "https://vellum.ai",
|
|
72
|
+
domain: "vellum.ai",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function findBlockById(
|
|
79
|
+
blocks: ContentBlock[],
|
|
80
|
+
id: string,
|
|
81
|
+
): Record<string, unknown> {
|
|
82
|
+
const block = (blocks as unknown as Array<Record<string, unknown>>).find(
|
|
83
|
+
(b) => b.id === id,
|
|
84
|
+
);
|
|
85
|
+
if (!block) throw new Error(`block ${id} not found`);
|
|
86
|
+
return block;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe("buildPersistedAssistantContent — native activityMetadata", () => {
|
|
92
|
+
let activity: Map<string, ToolActivityMetadata>;
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
activity = new Map();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("stamps captured activity onto a pure-native server_tool_use block", () => {
|
|
99
|
+
// GIVEN a turn whose only tool is a native server_tool_use whose activity
|
|
100
|
+
// was captured at server_tool_complete (no external tool_result, so the
|
|
101
|
+
// annotate pass never runs for this turn)
|
|
102
|
+
const nativeId = "srvtu_native_search";
|
|
103
|
+
activity.set(nativeId, webSearchActivity);
|
|
104
|
+
const rawBlocks = [
|
|
105
|
+
{ type: "text", text: "Let me search." },
|
|
106
|
+
{
|
|
107
|
+
type: "server_tool_use",
|
|
108
|
+
id: nativeId,
|
|
109
|
+
name: "web_search",
|
|
110
|
+
input: { query: "vellum docs" },
|
|
111
|
+
},
|
|
112
|
+
] as unknown as ContentBlock[];
|
|
113
|
+
|
|
114
|
+
// WHEN the content is built for persistence
|
|
115
|
+
const built = buildPersistedAssistantContent(rawBlocks, [], activity);
|
|
116
|
+
|
|
117
|
+
// THEN the server_tool_use block carries the native activity verbatim so it
|
|
118
|
+
// survives a history reopen
|
|
119
|
+
const block = findBlockById(built, nativeId);
|
|
120
|
+
expect(block._activityMetadata).toEqual(webSearchActivity);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("leaves a server_tool_use block untouched when no activity was captured", () => {
|
|
124
|
+
// GIVEN a native server_tool_use with no entry in the activity map
|
|
125
|
+
const nativeId = "srvtu_no_activity";
|
|
126
|
+
const rawBlocks = [
|
|
127
|
+
{
|
|
128
|
+
type: "server_tool_use",
|
|
129
|
+
id: nativeId,
|
|
130
|
+
name: "web_search",
|
|
131
|
+
input: { query: "vellum docs" },
|
|
132
|
+
},
|
|
133
|
+
] as unknown as ContentBlock[];
|
|
134
|
+
|
|
135
|
+
// WHEN the content is built for persistence
|
|
136
|
+
const built = buildPersistedAssistantContent(rawBlocks, [], activity);
|
|
137
|
+
|
|
138
|
+
// THEN no _activityMetadata is written
|
|
139
|
+
const block = findBlockById(built, nativeId);
|
|
140
|
+
expect(block._activityMetadata).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("does not stamp external tool_use blocks (handled by the annotate pass)", () => {
|
|
144
|
+
// GIVEN an external tool_use block whose id happens to have captured
|
|
145
|
+
// activity (external activity is stamped later in handleToolResult, not here)
|
|
146
|
+
const externalId = "tu_external_search";
|
|
147
|
+
activity.set(externalId, webSearchActivity);
|
|
148
|
+
const rawBlocks = [
|
|
149
|
+
{
|
|
150
|
+
type: "tool_use",
|
|
151
|
+
id: externalId,
|
|
152
|
+
name: "web_search",
|
|
153
|
+
input: { query: "vellum docs" },
|
|
154
|
+
},
|
|
155
|
+
] as unknown as ContentBlock[];
|
|
156
|
+
|
|
157
|
+
// WHEN the content is built for persistence
|
|
158
|
+
const built = buildPersistedAssistantContent(rawBlocks, [], activity);
|
|
159
|
+
|
|
160
|
+
// THEN the tool_use block is left untouched by this function
|
|
161
|
+
const block = findBlockById(built, externalId);
|
|
162
|
+
expect(block._activityMetadata).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("does not stamp when no activity map is provided", () => {
|
|
166
|
+
// GIVEN a native server_tool_use block AND no activity map argument
|
|
167
|
+
const nativeId = "srvtu_no_map";
|
|
168
|
+
const rawBlocks = [
|
|
169
|
+
{
|
|
170
|
+
type: "server_tool_use",
|
|
171
|
+
id: nativeId,
|
|
172
|
+
name: "web_search",
|
|
173
|
+
input: { query: "vellum docs" },
|
|
174
|
+
},
|
|
175
|
+
] as unknown as ContentBlock[];
|
|
176
|
+
|
|
177
|
+
// WHEN the content is built for persistence without the optional map
|
|
178
|
+
const built = buildPersistedAssistantContent(rawBlocks, []);
|
|
179
|
+
|
|
180
|
+
// THEN no _activityMetadata is written
|
|
181
|
+
const block = findBlockById(built, nativeId);
|
|
182
|
+
expect(block._activityMetadata).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -963,7 +963,7 @@ describe("createVellumCatalogProvider", () => {
|
|
|
963
963
|
expect(slim!.kind).toBe("catalog");
|
|
964
964
|
expect(slim!.origin).toBe("vellum");
|
|
965
965
|
expect(slim!.status).toBe("available");
|
|
966
|
-
expect(slim!.category).toBe("
|
|
966
|
+
expect(slim!.category).toBe("system");
|
|
967
967
|
});
|
|
968
968
|
|
|
969
969
|
test("toSlimSkill returns null for unknown skill", async () => {
|
|
@@ -16,7 +16,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
const _conversationMocks = new Map<string, unknown>();
|
|
19
|
-
mock.module("../daemon/conversation-
|
|
19
|
+
mock.module("../daemon/conversation-registry.js", () => ({
|
|
20
20
|
findConversation: (id: string) => _conversationMocks.get(id),
|
|
21
21
|
}));
|
|
22
22
|
|
|
@@ -11,7 +11,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
11
11
|
|
|
12
12
|
// Map conversationId → mock session so findConversation returns the right mock.
|
|
13
13
|
const conversationMocks = new Map<string, unknown>();
|
|
14
|
-
mock.module("../daemon/conversation-
|
|
14
|
+
mock.module("../daemon/conversation-registry.js", () => ({
|
|
15
15
|
findConversation: (id: string) => conversationMocks.get(id),
|
|
16
16
|
}));
|
|
17
17
|
|
|
@@ -291,7 +291,7 @@ describe("toSlimSkill", () => {
|
|
|
291
291
|
expect(slim!.kind).toBe("catalog");
|
|
292
292
|
expect(slim!.status).toBe("available");
|
|
293
293
|
expect(slim!.origin).toBe("clawhub");
|
|
294
|
-
expect(slim!.category).toBe("
|
|
294
|
+
expect(slim!.category).toBe("integrations");
|
|
295
295
|
|
|
296
296
|
// Clawhub-specific fields
|
|
297
297
|
const clawhub = slim as unknown as Record<string, unknown>;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `CompactionCircuit` class.
|
|
3
|
+
*
|
|
4
|
+
* The breaker logic lives as direct methods on the per-conversation
|
|
5
|
+
* `CompactionCircuit` state container (`agent/compaction-circuit.ts`). These
|
|
6
|
+
* tests assert the threshold (3 consecutive failures) and cooldown (1 hour)
|
|
7
|
+
* exactly match the user-visible behavior:
|
|
8
|
+
* (a) counter increments on each failure outcome
|
|
9
|
+
* (b) circuit opens after exactly 3 consecutive failures
|
|
10
|
+
* (c) successful compaction resets counter and clears the circuit
|
|
11
|
+
* (d) isOpen() reflects state and cooldown expiry
|
|
12
|
+
* (e) circuit re-opens after cooldown expiry when 3 more failures
|
|
13
|
+
* accumulate (guards the stale-timestamp regression)
|
|
14
|
+
* (f) isOpen() is query-only and never mutates the counter
|
|
15
|
+
* (g) open→closed transition emits `compaction_circuit_closed` exactly once
|
|
16
|
+
* (h) closed→closed transition emits nothing
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
COMPACTION_CIRCUIT_COOLDOWN_MS,
|
|
23
|
+
COMPACTION_CIRCUIT_FAILURE_THRESHOLD,
|
|
24
|
+
CompactionCircuit,
|
|
25
|
+
} from "../agent/compaction-circuit.js";
|
|
26
|
+
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
27
|
+
|
|
28
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const CONVERSATION_ID = "conv-breaker-test";
|
|
31
|
+
|
|
32
|
+
function collectEvents(): {
|
|
33
|
+
events: CompactionCircuitEvent[];
|
|
34
|
+
onEvent: (msg: CompactionCircuitEvent) => void;
|
|
35
|
+
} {
|
|
36
|
+
const events: CompactionCircuitEvent[] = [];
|
|
37
|
+
return { events, onEvent: (msg) => events.push(msg) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("CompactionCircuit", () => {
|
|
41
|
+
let originalDateNow: () => number;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
originalDateNow = Date.now;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
Date.now = originalDateNow;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("threshold and cooldown constants match the user-visible contract", () => {
|
|
52
|
+
expect(COMPACTION_CIRCUIT_FAILURE_THRESHOLD).toBe(3);
|
|
53
|
+
expect(COMPACTION_CIRCUIT_COOLDOWN_MS).toBe(60 * 60 * 1000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("(a) counter increments on each failure outcome", async () => {
|
|
57
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
58
|
+
const { onEvent, events } = collectEvents();
|
|
59
|
+
|
|
60
|
+
await circuit.recordOutcome(true, onEvent);
|
|
61
|
+
expect(circuit.consecutiveCompactionFailures).toBe(1);
|
|
62
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
63
|
+
expect(events).toHaveLength(0);
|
|
64
|
+
|
|
65
|
+
await circuit.recordOutcome(true, onEvent);
|
|
66
|
+
expect(circuit.consecutiveCompactionFailures).toBe(2);
|
|
67
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
68
|
+
expect(events).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("(b) circuit opens after exactly 3 consecutive failures", async () => {
|
|
72
|
+
const fixedNow = 1_700_000_000_000;
|
|
73
|
+
Date.now = () => fixedNow;
|
|
74
|
+
|
|
75
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
76
|
+
const { onEvent, events } = collectEvents();
|
|
77
|
+
|
|
78
|
+
await circuit.recordOutcome(true, onEvent);
|
|
79
|
+
await circuit.recordOutcome(true, onEvent);
|
|
80
|
+
// Two failures — circuit still closed.
|
|
81
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
82
|
+
expect(events).toHaveLength(0);
|
|
83
|
+
|
|
84
|
+
await circuit.recordOutcome(true, onEvent);
|
|
85
|
+
// Third failure — circuit trips and fires the event exactly once.
|
|
86
|
+
expect(circuit.consecutiveCompactionFailures).toBe(3);
|
|
87
|
+
expect(circuit.compactionCircuitOpenUntil).toBe(fixedNow + 60 * 60 * 1000);
|
|
88
|
+
expect(await circuit.isOpen()).toBe(true);
|
|
89
|
+
expect(events).toHaveLength(1);
|
|
90
|
+
expect(events[0]).toEqual({
|
|
91
|
+
type: "compaction_circuit_open",
|
|
92
|
+
conversationId: CONVERSATION_ID,
|
|
93
|
+
reason: "3_consecutive_failures",
|
|
94
|
+
openUntil: fixedNow + 60 * 60 * 1000,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Further failures do not re-fire the event while the circuit is open.
|
|
98
|
+
await circuit.recordOutcome(true, onEvent);
|
|
99
|
+
expect(circuit.consecutiveCompactionFailures).toBe(4);
|
|
100
|
+
expect(events).toHaveLength(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("(c) successful outcome resets counter and clears circuit", async () => {
|
|
104
|
+
const fixedNow = 1_700_000_000_000;
|
|
105
|
+
Date.now = () => fixedNow;
|
|
106
|
+
|
|
107
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
108
|
+
const { onEvent } = collectEvents();
|
|
109
|
+
|
|
110
|
+
// Trip the breaker.
|
|
111
|
+
await circuit.recordOutcome(true, onEvent);
|
|
112
|
+
await circuit.recordOutcome(true, onEvent);
|
|
113
|
+
await circuit.recordOutcome(true, onEvent);
|
|
114
|
+
expect(circuit.compactionCircuitOpenUntil).not.toBeNull();
|
|
115
|
+
|
|
116
|
+
// Success resets state.
|
|
117
|
+
await circuit.recordOutcome(false, onEvent);
|
|
118
|
+
expect(circuit.consecutiveCompactionFailures).toBe(0);
|
|
119
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("(d) isOpen() reflects state and expiry", async () => {
|
|
123
|
+
const fixedNow = 1_700_000_000_000;
|
|
124
|
+
Date.now = () => fixedNow;
|
|
125
|
+
|
|
126
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
127
|
+
const { onEvent } = collectEvents();
|
|
128
|
+
|
|
129
|
+
// Fresh state: closed.
|
|
130
|
+
expect(await circuit.isOpen()).toBe(false);
|
|
131
|
+
|
|
132
|
+
// Trip the breaker.
|
|
133
|
+
await circuit.recordOutcome(true, onEvent);
|
|
134
|
+
await circuit.recordOutcome(true, onEvent);
|
|
135
|
+
await circuit.recordOutcome(true, onEvent);
|
|
136
|
+
|
|
137
|
+
// While open.
|
|
138
|
+
expect(await circuit.isOpen()).toBe(true);
|
|
139
|
+
|
|
140
|
+
// After cooldown expires the breaker reports closed again, even without
|
|
141
|
+
// an explicit reset — the open-until timestamp is the only source of
|
|
142
|
+
// truth for the gate.
|
|
143
|
+
Date.now = () => fixedNow + 60 * 60 * 1000 + 1;
|
|
144
|
+
expect(await circuit.isOpen()).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("(e) circuit re-opens after cooldown expiry when 3 more failures accumulate", async () => {
|
|
148
|
+
// Regression: opening the breaker a second time must not require
|
|
149
|
+
// `compactionCircuitOpenUntil === null`. Once a cooldown expires, the
|
|
150
|
+
// gate correctly reports "closed" but a stale past-timestamp stays on the
|
|
151
|
+
// state, so the next 3-strike window must still trip a new cooldown. Any
|
|
152
|
+
// expired timestamp is treated the same as null.
|
|
153
|
+
const t0 = 1_700_000_000_000;
|
|
154
|
+
Date.now = () => t0;
|
|
155
|
+
|
|
156
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
157
|
+
const { onEvent, events } = collectEvents();
|
|
158
|
+
|
|
159
|
+
// Trip the breaker the first time.
|
|
160
|
+
await circuit.recordOutcome(true, onEvent);
|
|
161
|
+
await circuit.recordOutcome(true, onEvent);
|
|
162
|
+
await circuit.recordOutcome(true, onEvent);
|
|
163
|
+
expect(circuit.compactionCircuitOpenUntil).toBe(t0 + 60 * 60 * 1000);
|
|
164
|
+
expect(events).toHaveLength(1);
|
|
165
|
+
|
|
166
|
+
// Advance past the cooldown window. Reset the counter — in production this
|
|
167
|
+
// happens when a subsequent `maybeCompact` call succeeds after the
|
|
168
|
+
// cooldown elapses, but the bug manifests even when the counter is reset:
|
|
169
|
+
// the stale `compactionCircuitOpenUntil` is what breaks re-opening.
|
|
170
|
+
const t1 = t0 + 60 * 60 * 1000 + 1;
|
|
171
|
+
Date.now = () => t1;
|
|
172
|
+
expect(await circuit.isOpen()).toBe(false);
|
|
173
|
+
circuit.consecutiveCompactionFailures = 0;
|
|
174
|
+
// `compactionCircuitOpenUntil` is deliberately left as the old timestamp
|
|
175
|
+
// to reproduce the bug condition.
|
|
176
|
+
expect(circuit.compactionCircuitOpenUntil).toBe(t0 + 60 * 60 * 1000);
|
|
177
|
+
|
|
178
|
+
// Three more failures must trip a fresh cooldown even though the old
|
|
179
|
+
// timestamp is still set.
|
|
180
|
+
await circuit.recordOutcome(true, onEvent);
|
|
181
|
+
await circuit.recordOutcome(true, onEvent);
|
|
182
|
+
await circuit.recordOutcome(true, onEvent);
|
|
183
|
+
expect(circuit.consecutiveCompactionFailures).toBe(3);
|
|
184
|
+
expect(circuit.compactionCircuitOpenUntil).toBe(t1 + 60 * 60 * 1000);
|
|
185
|
+
expect(events).toHaveLength(2);
|
|
186
|
+
expect(events[1]).toEqual({
|
|
187
|
+
type: "compaction_circuit_open",
|
|
188
|
+
conversationId: CONVERSATION_ID,
|
|
189
|
+
reason: "3_consecutive_failures",
|
|
190
|
+
openUntil: t1 + 60 * 60 * 1000,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("(f) isOpen() is query-only and never mutates the counter", async () => {
|
|
195
|
+
// `maybeCompact()` early-return paths skip `recordOutcome` entirely and
|
|
196
|
+
// only gate on `isOpen()`. The query must never touch the 3-strike
|
|
197
|
+
// counter so those early returns can't silently reset it.
|
|
198
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
199
|
+
const { onEvent } = collectEvents();
|
|
200
|
+
|
|
201
|
+
await circuit.recordOutcome(true, onEvent);
|
|
202
|
+
await circuit.recordOutcome(true, onEvent);
|
|
203
|
+
expect(circuit.consecutiveCompactionFailures).toBe(2);
|
|
204
|
+
|
|
205
|
+
// Query-only — must NOT touch the counter.
|
|
206
|
+
expect(await circuit.isOpen()).toBe(false);
|
|
207
|
+
expect(circuit.consecutiveCompactionFailures).toBe(2);
|
|
208
|
+
|
|
209
|
+
// A third real failure then trips the breaker as expected.
|
|
210
|
+
await circuit.recordOutcome(true, onEvent);
|
|
211
|
+
expect(circuit.consecutiveCompactionFailures).toBe(3);
|
|
212
|
+
expect(circuit.compactionCircuitOpenUntil).not.toBeNull();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("(g) open→closed transition emits compaction_circuit_closed exactly once", async () => {
|
|
216
|
+
// Regression: the reset branch must notify the client on open→closed.
|
|
217
|
+
// Otherwise the Swift banner set from `compaction_circuit_open` stays
|
|
218
|
+
// visible until the original `openUntil` deadline (up to 1h),
|
|
219
|
+
// misrepresenting the live state.
|
|
220
|
+
const fixedNow = 1_700_000_000_000;
|
|
221
|
+
Date.now = () => fixedNow;
|
|
222
|
+
|
|
223
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
224
|
+
const { onEvent, events } = collectEvents();
|
|
225
|
+
|
|
226
|
+
// Force the circuit into the open state directly — the emitted-event
|
|
227
|
+
// transition logic is what we're testing, not the tripping path.
|
|
228
|
+
circuit.compactionCircuitOpenUntil = fixedNow + 60 * 60 * 1000;
|
|
229
|
+
circuit.consecutiveCompactionFailures = 3;
|
|
230
|
+
|
|
231
|
+
await circuit.recordOutcome(false, onEvent);
|
|
232
|
+
|
|
233
|
+
expect(circuit.consecutiveCompactionFailures).toBe(0);
|
|
234
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
235
|
+
expect(events).toHaveLength(1);
|
|
236
|
+
expect(events[0]).toEqual({
|
|
237
|
+
type: "compaction_circuit_closed",
|
|
238
|
+
conversationId: CONVERSATION_ID,
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("(h) successful outcome against an already-closed circuit emits no event", async () => {
|
|
243
|
+
// Emitting `compaction_circuit_closed` on every successful compaction
|
|
244
|
+
// would spam the client (the breaker is closed in the common case).
|
|
245
|
+
// Only the open→closed transition is meaningful.
|
|
246
|
+
const circuit = new CompactionCircuit(CONVERSATION_ID);
|
|
247
|
+
const { onEvent, events } = collectEvents();
|
|
248
|
+
|
|
249
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
250
|
+
await circuit.recordOutcome(false, onEvent);
|
|
251
|
+
expect(circuit.compactionCircuitOpenUntil).toBeNull();
|
|
252
|
+
expect(events).toHaveLength(0);
|
|
253
|
+
|
|
254
|
+
// A second successful outcome while still closed — still no event.
|
|
255
|
+
await circuit.recordOutcome(false, onEvent);
|
|
256
|
+
expect(events).toHaveLength(0);
|
|
257
|
+
});
|
|
258
|
+
});
|