@vellumai/assistant 0.8.8 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- 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/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.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__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- 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__/history-repair-hook.test.ts +1 -1
- 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__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- 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__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- 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 +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -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 +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- 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/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- 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/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -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-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- 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/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.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/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- 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__/live-integration.test.ts +9 -6
- 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 +8 -5
- 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 +10 -5
- 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 +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- 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/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- 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 +5 -5
- 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 +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- 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/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- 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 +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- 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-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- 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__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- 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/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- 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/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /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__/health.test.ts +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__/provider-blocks.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__/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}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +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
|
@@ -22,7 +22,6 @@ import type { Message } from "../providers/types.js";
|
|
|
22
22
|
|
|
23
23
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
24
24
|
getConversationOverrideProfile: () => undefined,
|
|
25
|
-
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
26
25
|
}));
|
|
27
26
|
|
|
28
27
|
mock.module("../config/loader.js", () => ({
|
|
@@ -62,36 +61,37 @@ mock.module("../daemon/disk-pressure-guard.js", () => ({
|
|
|
62
61
|
}),
|
|
63
62
|
}));
|
|
64
63
|
|
|
64
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
65
65
|
import {
|
|
66
66
|
__resetWakeChainForTests,
|
|
67
67
|
wakeAgentForOpportunity,
|
|
68
|
-
type WakeTarget,
|
|
69
68
|
} from "../runtime/agent-wake.js";
|
|
70
69
|
|
|
71
|
-
function makeTarget():
|
|
72
|
-
const
|
|
70
|
+
function makeTarget(): Conversation {
|
|
71
|
+
const messages: Message[] = [];
|
|
73
72
|
let processing = false;
|
|
74
|
-
|
|
73
|
+
const target = {
|
|
75
74
|
conversationId: "conv-wake-callsite",
|
|
76
75
|
agentLoop: {
|
|
77
|
-
run:
|
|
76
|
+
run: async ({ messages }: { messages: Message[] }) => ({
|
|
78
77
|
history: messages,
|
|
79
78
|
exitReason: null,
|
|
80
79
|
appendedNewMessages: false,
|
|
81
80
|
newMessages: [],
|
|
82
|
-
})
|
|
81
|
+
}),
|
|
83
82
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
history.push(msg);
|
|
87
|
-
},
|
|
88
|
-
emitAgentEvent: () => {},
|
|
83
|
+
messages,
|
|
84
|
+
getMessages: () => messages,
|
|
89
85
|
isProcessing: () => processing,
|
|
90
|
-
|
|
86
|
+
setProcessing: (on: boolean) => {
|
|
91
87
|
processing = on;
|
|
92
88
|
},
|
|
93
|
-
|
|
89
|
+
setTrustContext: () => {},
|
|
90
|
+
getTurnChannelContext: () => null,
|
|
91
|
+
getTurnInterfaceContext: () => null,
|
|
92
|
+
drainQueue: async () => {},
|
|
94
93
|
};
|
|
94
|
+
return target as unknown as Conversation;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
beforeEach(() => {
|
|
@@ -48,8 +48,9 @@ mock.module("../config/loader.js", () => ({
|
|
|
48
48
|
}),
|
|
49
49
|
}));
|
|
50
50
|
|
|
51
|
-
import type {
|
|
51
|
+
import type { AgentLoopRunOptions } from "../agent/loop.js";
|
|
52
52
|
import { LLMSchema } from "../config/schemas/llm.js";
|
|
53
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
53
54
|
import { RetryProvider } from "../providers/retry.js";
|
|
54
55
|
import type {
|
|
55
56
|
Message,
|
|
@@ -60,7 +61,6 @@ import type {
|
|
|
60
61
|
import {
|
|
61
62
|
__resetWakeChainForTests,
|
|
62
63
|
wakeAgentForOpportunity,
|
|
63
|
-
type WakeTarget,
|
|
64
64
|
} from "../runtime/agent-wake.js";
|
|
65
65
|
|
|
66
66
|
interface RunArgs {
|
|
@@ -69,44 +69,40 @@ interface RunArgs {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function makeTarget(): {
|
|
72
|
-
target:
|
|
72
|
+
target: Conversation;
|
|
73
73
|
runArgs: RunArgs[];
|
|
74
74
|
} {
|
|
75
75
|
const runArgs: RunArgs[] = [];
|
|
76
|
-
const
|
|
76
|
+
const messages: Message[] = [
|
|
77
77
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
78
78
|
];
|
|
79
79
|
let processing = false;
|
|
80
80
|
|
|
81
|
-
const target
|
|
81
|
+
const target = {
|
|
82
82
|
conversationId: "conv-wake-override",
|
|
83
83
|
agentLoop: {
|
|
84
|
-
run:
|
|
85
|
-
messages: Message[],
|
|
86
|
-
_onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
87
|
-
options?: AgentLoopRunOptions,
|
|
88
|
-
) => {
|
|
84
|
+
run: async (options: AgentLoopRunOptions) => {
|
|
89
85
|
runArgs.push({
|
|
90
|
-
messages: [...messages],
|
|
86
|
+
messages: [...options.messages],
|
|
91
87
|
options,
|
|
92
88
|
});
|
|
93
89
|
// Return the input verbatim → silent no-op (no assistant tail).
|
|
94
90
|
// Wake never yields at a checkpoint, so the pause-reason is null.
|
|
95
|
-
return { history: messages, exitReason: null };
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
getMessages: () => history,
|
|
99
|
-
pushMessage: (msg) => {
|
|
100
|
-
history.push(msg);
|
|
91
|
+
return { history: options.messages, exitReason: null };
|
|
92
|
+
},
|
|
101
93
|
},
|
|
102
|
-
|
|
94
|
+
messages,
|
|
95
|
+
getMessages: () => messages,
|
|
103
96
|
isProcessing: () => processing,
|
|
104
|
-
|
|
97
|
+
setProcessing: (on: boolean) => {
|
|
105
98
|
processing = on;
|
|
106
99
|
},
|
|
107
|
-
|
|
100
|
+
setTrustContext: () => {},
|
|
101
|
+
getTurnChannelContext: () => null,
|
|
102
|
+
getTurnInterfaceContext: () => null,
|
|
103
|
+
drainQueue: async () => {},
|
|
108
104
|
};
|
|
109
|
-
return { target, runArgs };
|
|
105
|
+
return { target: target as unknown as Conversation, runArgs };
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
beforeEach(() => {
|
|
@@ -102,7 +102,7 @@ mock.module("@anthropic-ai/sdk", () => ({
|
|
|
102
102
|
}));
|
|
103
103
|
|
|
104
104
|
// Import after mocking
|
|
105
|
-
import { cachedTextBlock } from "../memory
|
|
105
|
+
import { cachedTextBlock } from "../plugins/defaults/memory-v3-shadow/provider-blocks.js";
|
|
106
106
|
import { AnthropicProvider } from "../providers/anthropic/client.js";
|
|
107
107
|
import {
|
|
108
108
|
isPlaceholderSentinelText,
|
|
@@ -81,6 +81,7 @@ mock.module("../bundler/app-compiler.js", () => ({
|
|
|
81
81
|
import * as appCreateScript from "../config/bundled-skills/app-builder/tools/app-create.js";
|
|
82
82
|
import * as appDeleteScript from "../config/bundled-skills/app-builder/tools/app-delete.js";
|
|
83
83
|
import * as appRefreshScript from "../config/bundled-skills/app-builder/tools/app-refresh.js";
|
|
84
|
+
import * as appUpdateScript from "../config/bundled-skills/app-builder/tools/app-update.js";
|
|
84
85
|
|
|
85
86
|
// ---------------------------------------------------------------------------
|
|
86
87
|
// Tests
|
|
@@ -171,4 +172,24 @@ describe("app-builder skill tool scripts", () => {
|
|
|
171
172
|
expect(parsed.appId).toBe("app-1");
|
|
172
173
|
});
|
|
173
174
|
});
|
|
175
|
+
|
|
176
|
+
// ---- app-update --------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe("app-update", () => {
|
|
179
|
+
test("exports a run function", () => {
|
|
180
|
+
expect(typeof appUpdateScript.run).toBe("function");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("delegates to executeAppUpdate and returns result", async () => {
|
|
184
|
+
const result = await appUpdateScript.run(
|
|
185
|
+
{ app_id: "app-1", name: "Renamed" },
|
|
186
|
+
makeContext(),
|
|
187
|
+
);
|
|
188
|
+
expect(result.isError).toBe(false);
|
|
189
|
+
const parsed = JSON.parse(result.content);
|
|
190
|
+
expect(parsed.updated).toBe(true);
|
|
191
|
+
expect(parsed.appId).toBe("app-1");
|
|
192
|
+
expect(parsed.name).toBe("Renamed");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
174
195
|
});
|
|
@@ -103,7 +103,7 @@ const { ROUTES } = await import("../runtime/routes/host-app-control-routes.js");
|
|
|
103
103
|
const { surfaceProxyResolver } =
|
|
104
104
|
await import("../daemon/conversation-surfaces.js");
|
|
105
105
|
const { setConversation, clearConversations } =
|
|
106
|
-
await import("../daemon/conversation-
|
|
106
|
+
await import("../daemon/conversation-registry.js");
|
|
107
107
|
type SurfaceConversationContext =
|
|
108
108
|
import("../daemon/conversation-surfaces.js").SurfaceConversationContext;
|
|
109
109
|
|
|
@@ -19,6 +19,7 @@ const ALLOWLIST = new Set([
|
|
|
19
19
|
"assistant/src/memory/app-store.ts", // defines getAppsDir
|
|
20
20
|
"assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
|
|
21
21
|
"assistant/src/daemon/app-source-watcher.ts", // uses getAppsDir for recursive fs.watch root, not per-app paths
|
|
22
|
+
"assistant/src/tools/filesystem/write.ts", // uses getAppsDir as an exemption root for the artifact-HTML guard, not per-app paths
|
|
22
23
|
]);
|
|
23
24
|
|
|
24
25
|
function isTestFile(filePath: string): boolean {
|
|
@@ -32,6 +32,7 @@ import type { AppStore } from "../tools/apps/executors.js";
|
|
|
32
32
|
import {
|
|
33
33
|
executeAppCreate,
|
|
34
34
|
executeAppRefresh,
|
|
35
|
+
executeAppUpdate,
|
|
35
36
|
} from "../tools/apps/executors.js";
|
|
36
37
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
@@ -89,6 +90,46 @@ function mockStore(
|
|
|
89
90
|
// ---------------------------------------------------------------------------
|
|
90
91
|
|
|
91
92
|
describe("executeAppCreate", () => {
|
|
93
|
+
test("falls back to the preview title when the name is omitted or blank", async () => {
|
|
94
|
+
let createdParams: Record<string, unknown> | undefined;
|
|
95
|
+
const app = makeMultifileApp({ name: "Coffee Tracker" });
|
|
96
|
+
const store: AppStore = {
|
|
97
|
+
...mockStore(app, {}),
|
|
98
|
+
createApp: (params) => {
|
|
99
|
+
createdParams = params as unknown as Record<string, unknown>;
|
|
100
|
+
return app;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await executeAppCreate(
|
|
105
|
+
{ name: " ", preview: { title: "Coffee Tracker" } },
|
|
106
|
+
store,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(result.isError).toBe(false);
|
|
110
|
+
expect(createdParams?.name).toBe("Coffee Tracker");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("defaults the name to 'New App' when neither name nor preview title is given", async () => {
|
|
114
|
+
let createdParams: Record<string, unknown> | undefined;
|
|
115
|
+
const app = makeMultifileApp({ name: "New App" });
|
|
116
|
+
const store: AppStore = {
|
|
117
|
+
...mockStore(app, {}),
|
|
118
|
+
createApp: (params) => {
|
|
119
|
+
createdParams = params as unknown as Record<string, unknown>;
|
|
120
|
+
return app;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await executeAppCreate(
|
|
125
|
+
{} as unknown as Parameters<typeof executeAppCreate>[0],
|
|
126
|
+
store,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(result.isError).toBe(false);
|
|
130
|
+
expect(createdParams?.name).toBe("New App");
|
|
131
|
+
});
|
|
132
|
+
|
|
92
133
|
test("creates multifile app with src/ scaffold", async () => {
|
|
93
134
|
const files: Record<string, string> = {};
|
|
94
135
|
let createdParams: Record<string, unknown> | undefined;
|
|
@@ -408,3 +449,94 @@ describe("executeAppRefresh", () => {
|
|
|
408
449
|
expect(parsed.error).toContain("not found");
|
|
409
450
|
});
|
|
410
451
|
});
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// executeAppUpdate
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
describe("executeAppUpdate", () => {
|
|
458
|
+
test("updates metadata and recompiles a multifile app", async () => {
|
|
459
|
+
const app = makeMultifileApp({ name: "Old", description: "old desc" });
|
|
460
|
+
let updateArgs: unknown;
|
|
461
|
+
const store: AppStore = {
|
|
462
|
+
...mockStore(app),
|
|
463
|
+
updateApp: (_id, updates) => {
|
|
464
|
+
updateArgs = updates;
|
|
465
|
+
return { ...app, ...updates };
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const result = await executeAppUpdate(
|
|
470
|
+
{ app_id: app.id, name: "New Name", description: "new desc" },
|
|
471
|
+
store,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(result.isError).toBe(false);
|
|
475
|
+
expect(updateArgs).toEqual({ name: "New Name", description: "new desc" });
|
|
476
|
+
const parsed = JSON.parse(result.content);
|
|
477
|
+
expect(parsed.updated).toBe(true);
|
|
478
|
+
expect(parsed.name).toBe("New Name");
|
|
479
|
+
expect(parsed.description).toBe("new desc");
|
|
480
|
+
expect(parsed.compiled).toBe(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("writes source_files before recompiling", async () => {
|
|
484
|
+
const files: Record<string, string> = {};
|
|
485
|
+
const app = makeMultifileApp();
|
|
486
|
+
const store: AppStore = {
|
|
487
|
+
...mockStore(app, files),
|
|
488
|
+
updateApp: (_id, updates) => ({ ...app, ...updates }),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const result = await executeAppUpdate(
|
|
492
|
+
{ app_id: app.id, source_files: { "src/App.tsx": "// new code" } },
|
|
493
|
+
store,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(result.isError).toBe(false);
|
|
497
|
+
expect(files["src/App.tsx"]).toBe("// new code");
|
|
498
|
+
expect(JSON.parse(result.content).compiled).toBe(true);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("legacy app: updates without compiling", async () => {
|
|
502
|
+
const app = makeLegacyApp({ name: "Legacy" });
|
|
503
|
+
const store: AppStore = {
|
|
504
|
+
...mockStore(app),
|
|
505
|
+
updateApp: (_id, updates) => ({ ...app, ...updates }),
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const result = await executeAppUpdate(
|
|
509
|
+
{ app_id: app.id, name: "Renamed" },
|
|
510
|
+
store,
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(result.isError).toBe(false);
|
|
514
|
+
const parsed = JSON.parse(result.content);
|
|
515
|
+
expect(parsed.updated).toBe(true);
|
|
516
|
+
expect(parsed.name).toBe("Renamed");
|
|
517
|
+
expect(parsed.compiled).toBeUndefined();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("returns error for unknown app", async () => {
|
|
521
|
+
const store = mockStore(makeMultifileApp());
|
|
522
|
+
const result = await executeAppUpdate({ app_id: "nope" }, store);
|
|
523
|
+
|
|
524
|
+
expect(result.isError).toBe(true);
|
|
525
|
+
expect(JSON.parse(result.content).error).toContain("not found");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("rejects invalid source_files", async () => {
|
|
529
|
+
const app = makeMultifileApp();
|
|
530
|
+
const store = mockStore(app);
|
|
531
|
+
const result = await executeAppUpdate(
|
|
532
|
+
{
|
|
533
|
+
app_id: app.id,
|
|
534
|
+
source_files: { "src/App.tsx": 123 as unknown as string },
|
|
535
|
+
},
|
|
536
|
+
store,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(result.isError).toBe(true);
|
|
540
|
+
expect(JSON.parse(result.content).error).toContain("must be a string");
|
|
541
|
+
});
|
|
542
|
+
});
|
|
@@ -176,6 +176,7 @@ mock.module("../memory/retriever.js", () => ({
|
|
|
176
176
|
mock.module("../context/window-manager.js", () => ({
|
|
177
177
|
ContextWindowManager: class {
|
|
178
178
|
constructor() {}
|
|
179
|
+
updateConfig() {}
|
|
179
180
|
shouldCompact() {
|
|
180
181
|
return { needed: false, estimatedTokens: 0 };
|
|
181
182
|
}
|
|
@@ -208,10 +209,10 @@ mock.module("../agent/loop.js", () => ({
|
|
|
208
209
|
getActiveModel() {
|
|
209
210
|
return undefined;
|
|
210
211
|
}
|
|
211
|
-
async run(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
): Promise<Message[]> {
|
|
212
|
+
async run(_options: {
|
|
213
|
+
messages: Message[];
|
|
214
|
+
onEvent: (event: AgentEvent) => void;
|
|
215
|
+
}): Promise<Message[]> {
|
|
215
216
|
return [];
|
|
216
217
|
}
|
|
217
218
|
},
|
|
@@ -119,12 +119,15 @@ mock.module("../permissions/trust-store.js", () => ({
|
|
|
119
119
|
// ---------------------------------------------------------------------------
|
|
120
120
|
let _conversationFactory: (() => Conversation) | undefined;
|
|
121
121
|
|
|
122
|
-
mock.module("../daemon/conversation-
|
|
122
|
+
mock.module("../daemon/conversation-registry.js", () => ({
|
|
123
123
|
findConversation: () => {
|
|
124
124
|
// Return the current test session for any conversation ID lookup.
|
|
125
125
|
if (!_conversationFactory) return undefined;
|
|
126
126
|
return _conversationFactory();
|
|
127
127
|
},
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
mock.module("../daemon/conversation-store.js", () => ({
|
|
128
131
|
getOrCreateConversation: async () => {
|
|
129
132
|
if (!_conversationFactory)
|
|
130
133
|
throw new Error("_conversationFactory not set in test");
|
|
@@ -117,7 +117,7 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
117
117
|
findMostRecentRetrospectiveFor: mock(() => null),
|
|
118
118
|
forkConversation: mock(() => ({ id: "conv-fork" })),
|
|
119
119
|
getConversationOverrideProfile: () => undefined,
|
|
120
|
-
|
|
120
|
+
resolveOverrideProfile: () => undefined,
|
|
121
121
|
getConversationMemoryScopeId: () => "default",
|
|
122
122
|
getConversationOriginChannel: () => null,
|
|
123
123
|
getConversationOriginInterface: () => null,
|
|
@@ -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
|
|
|
@@ -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
|
+
});
|