@vellumai/assistant 0.8.7 → 0.8.8
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/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/docs/plugins.md +75 -79
- package/examples/plugins/echo/README.md +6 -12
- package/examples/plugins/echo/register.ts +0 -41
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
- package/openapi.yaml +3381 -348
- package/package.json +1 -1
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
- 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__/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__/clawhub-files.test.ts +1 -1
- package/src/__tests__/compaction-pipeline.test.ts +1 -1
- 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 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
- package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- 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 +26 -5
- 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 +170 -229
- package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- 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-injection.test.ts +6 -1
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -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__/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 +2 -8
- package/src/__tests__/injector-chain.test.ts +106 -270
- package/src/__tests__/injector-disk-pressure.test.ts +3 -12
- package/src/__tests__/injector-document-comments.test.ts +2 -2
- 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-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__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pipeline-runner.test.ts +29 -39
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-bootstrap.test.ts +13 -28
- package/src/__tests__/plugin-registry.test.ts +0 -27
- package/src/__tests__/plugin-types.test.ts +2 -125
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- 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__/server-history-render.test.ts +314 -1
- package/src/__tests__/skillssh-files.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 +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- package/src/agent/loop.ts +346 -442
- 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 +368 -0
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- package/src/config/assistant-feature-flags.ts +22 -11
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
- 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/document-editor/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -7
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +35 -3
- 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/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 +122 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +3 -6
- 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 +605 -153
- package/src/daemon/conversation-agent-loop.ts +281 -760
- 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-runtime-assembly.ts +130 -347
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation.ts +32 -14
- package/src/daemon/external-plugins-bootstrap.ts +9 -10
- 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/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +39 -10
- package/src/daemon/lifecycle.ts +4 -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/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- 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/home/home-greeting-cache.ts +24 -1
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.ts +3 -3
- 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-title-service.ts +65 -41
- package/src/memory/db-init.ts +4 -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/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +31 -4
- 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/index.ts +2 -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/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
- package/src/memory/v2/consolidation-job.ts +1 -1
- package/src/memory/v3/__tests__/health.test.ts +16 -0
- package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
- package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
- package/src/memory/v3/__tests__/router.test.ts +101 -29
- package/src/memory/v3/__tests__/selector.test.ts +93 -27
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/llm-retry.ts +32 -0
- package/src/memory/v3/orchestrate.ts +26 -14
- package/src/memory/v3/provider-blocks.ts +15 -5
- package/src/memory/v3/router.ts +48 -42
- package/src/memory/v3/selector.ts +57 -42
- package/src/memory/v3/shadow-plugin.ts +47 -15
- package/src/memory/v3/types.ts +8 -0
- 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 +8 -1
- package/src/plugin-api/types.ts +151 -1
- 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 +1 -15
- package/src/plugins/defaults/injectors/register.ts +243 -74
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -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/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/pipeline.ts +6 -18
- package/src/plugins/registry.ts +8 -25
- package/src/plugins/types.ts +43 -474
- package/src/proactive-artifact/aux-message-injector.ts +3 -3
- package/src/proactive-artifact/job.test.ts +7 -12
- package/src/prompts/__tests__/system-prompt.test.ts +36 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -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 +5 -1
- package/src/runtime/agent-wake.ts +2 -2
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- 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__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- package/src/runtime/routes/conversation-query-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +358 -184
- 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/identity-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +208 -17
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- 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 +50 -28
- 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/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/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/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/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- 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/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/executor.ts +1 -53
- 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/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +9 -1
- 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/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 +93 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- 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__/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/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/package.json +0 -15
- 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/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
|
@@ -4,34 +4,42 @@ import type { AssistantEvent } from "../runtime/assistant-event.js";
|
|
|
4
4
|
import type {
|
|
5
5
|
EventTargeting,
|
|
6
6
|
ReplaySubscriber,
|
|
7
|
-
} from "../runtime/
|
|
7
|
+
} from "../runtime/assistant-stream-state.js";
|
|
8
8
|
import {
|
|
9
9
|
_peekStreamForTesting,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
_resetStreamStateForTesting,
|
|
11
|
+
getCurrentSeq,
|
|
12
|
+
getPersistedSeq,
|
|
12
13
|
getReplayWindow,
|
|
14
|
+
recordPersistedSeq,
|
|
13
15
|
stampAndBuffer,
|
|
14
|
-
} from "../runtime/
|
|
16
|
+
} from "../runtime/assistant-stream-state.js";
|
|
15
17
|
|
|
16
18
|
const CONV = "conv_test";
|
|
17
19
|
|
|
18
20
|
function mkEvent(overrides: Partial<AssistantEvent> = {}): AssistantEvent {
|
|
21
|
+
const conversationId =
|
|
22
|
+
"conversationId" in overrides ? overrides.conversationId : CONV;
|
|
19
23
|
return {
|
|
20
24
|
id: `uuid-${Math.random().toString(36).slice(2, 10)}`,
|
|
21
|
-
conversationId
|
|
25
|
+
conversationId,
|
|
22
26
|
emittedAt: new Date().toISOString(),
|
|
23
|
-
message: {
|
|
27
|
+
message: {
|
|
28
|
+
type: "assistant_text_delta",
|
|
29
|
+
conversationId,
|
|
30
|
+
text: "x",
|
|
31
|
+
},
|
|
24
32
|
...overrides,
|
|
25
33
|
} as AssistantEvent;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
describe("
|
|
36
|
+
describe("assistant-stream-state", () => {
|
|
29
37
|
beforeEach(() => {
|
|
30
|
-
|
|
38
|
+
_resetStreamStateForTesting();
|
|
31
39
|
});
|
|
32
40
|
|
|
33
41
|
describe("stampAndBuffer", () => {
|
|
34
|
-
test("assigns monotonic seq starting at 1
|
|
42
|
+
test("assigns monotonic seq starting at 1", () => {
|
|
35
43
|
const a = mkEvent();
|
|
36
44
|
const b = mkEvent();
|
|
37
45
|
const c = mkEvent();
|
|
@@ -43,16 +51,26 @@ describe("conversation-stream-state", () => {
|
|
|
43
51
|
expect(c.seq).toBe(3);
|
|
44
52
|
});
|
|
45
53
|
|
|
46
|
-
test("seq is
|
|
54
|
+
test("seq is a single global counter shared across conversations", () => {
|
|
55
|
+
/**
|
|
56
|
+
* All conversations draw from one global seq space, so a reconnect
|
|
57
|
+
* cursor can be a single number rather than a per-conversation map.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// GIVEN events interleaved across two conversations
|
|
47
61
|
const a = mkEvent({ conversationId: "conv_a" });
|
|
48
62
|
const b = mkEvent({ conversationId: "conv_b" });
|
|
49
63
|
const a2 = mkEvent({ conversationId: "conv_a" });
|
|
64
|
+
|
|
65
|
+
// WHEN they are stamped
|
|
50
66
|
stampAndBuffer(a);
|
|
51
67
|
stampAndBuffer(b);
|
|
52
68
|
stampAndBuffer(a2);
|
|
69
|
+
|
|
70
|
+
// THEN seq is contiguous across conversations, not reset per conversation
|
|
53
71
|
expect(a.seq).toBe(1);
|
|
54
|
-
expect(b.seq).toBe(
|
|
55
|
-
expect(a2.seq).toBe(
|
|
72
|
+
expect(b.seq).toBe(2);
|
|
73
|
+
expect(a2.seq).toBe(3);
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
test("no-op when conversationId is absent (unscoped broadcasts)", () => {
|
|
@@ -61,17 +79,17 @@ describe("conversation-stream-state", () => {
|
|
|
61
79
|
expect(event.seq).toBeUndefined();
|
|
62
80
|
});
|
|
63
81
|
|
|
64
|
-
test("pushes event onto ring buffer", () => {
|
|
82
|
+
test("pushes event onto the ring buffer", () => {
|
|
65
83
|
stampAndBuffer(mkEvent());
|
|
66
84
|
stampAndBuffer(mkEvent());
|
|
67
|
-
const peek = _peekStreamForTesting(
|
|
68
|
-
expect(peek
|
|
69
|
-
expect(peek
|
|
70
|
-
expect(peek
|
|
85
|
+
const peek = _peekStreamForTesting();
|
|
86
|
+
expect(peek.ringLength).toBe(2);
|
|
87
|
+
expect(peek.oldestSeq).toBe(1);
|
|
88
|
+
expect(peek.newestSeq).toBe(2);
|
|
71
89
|
});
|
|
72
90
|
|
|
73
91
|
test("targeted events are buffered with targeting metadata", () => {
|
|
74
|
-
/** Targeted events
|
|
92
|
+
/** Targeted events stay in the ring so replay can filter them. */
|
|
75
93
|
|
|
76
94
|
// GIVEN a targeting modifier
|
|
77
95
|
const targeting: EventTargeting = {
|
|
@@ -84,9 +102,9 @@ describe("conversation-stream-state", () => {
|
|
|
84
102
|
|
|
85
103
|
// THEN it receives a seq and lands in the ring
|
|
86
104
|
expect(targeted.seq).toBe(1);
|
|
87
|
-
const peek = _peekStreamForTesting(
|
|
88
|
-
expect(peek
|
|
89
|
-
expect(peek
|
|
105
|
+
const peek = _peekStreamForTesting();
|
|
106
|
+
expect(peek.ringLength).toBe(1);
|
|
107
|
+
expect(peek.oldestSeq).toBe(1);
|
|
90
108
|
});
|
|
91
109
|
|
|
92
110
|
test("seq stays monotonic across targeted and untargeted events", () => {
|
|
@@ -106,21 +124,21 @@ describe("conversation-stream-state", () => {
|
|
|
106
124
|
|
|
107
125
|
// THEN seqs are monotonic and all four are buffered
|
|
108
126
|
expect([a.seq, b.seq, c.seq, d.seq]).toEqual([1, 2, 3, 4]);
|
|
109
|
-
const peek = _peekStreamForTesting(
|
|
110
|
-
expect(peek
|
|
111
|
-
expect(peek
|
|
112
|
-
expect(peek
|
|
127
|
+
const peek = _peekStreamForTesting();
|
|
128
|
+
expect(peek.ringLength).toBe(4);
|
|
129
|
+
expect(peek.oldestSeq).toBe(1);
|
|
130
|
+
expect(peek.newestSeq).toBe(4);
|
|
113
131
|
});
|
|
114
132
|
});
|
|
115
133
|
|
|
116
134
|
describe("ring buffer eviction", () => {
|
|
117
135
|
test("evicts oldest entries past the 200-event count cap", () => {
|
|
118
136
|
for (let i = 0; i < 250; i++) stampAndBuffer(mkEvent());
|
|
119
|
-
const peek = _peekStreamForTesting(
|
|
120
|
-
expect(peek
|
|
137
|
+
const peek = _peekStreamForTesting();
|
|
138
|
+
expect(peek.ringLength).toBe(200);
|
|
121
139
|
// Newest is 250, oldest should be 51 (250 - 200 + 1)
|
|
122
|
-
expect(peek
|
|
123
|
-
expect(peek
|
|
140
|
+
expect(peek.newestSeq).toBe(250);
|
|
141
|
+
expect(peek.oldestSeq).toBe(51);
|
|
124
142
|
});
|
|
125
143
|
|
|
126
144
|
test("evicts past the 256 KB size cap", () => {
|
|
@@ -137,14 +155,13 @@ describe("conversation-stream-state", () => {
|
|
|
137
155
|
}),
|
|
138
156
|
);
|
|
139
157
|
}
|
|
140
|
-
const peek = _peekStreamForTesting(
|
|
141
|
-
expect(peek).not.toBeNull();
|
|
158
|
+
const peek = _peekStreamForTesting();
|
|
142
159
|
// 60 * ~8KB = ~480KB pushed; ring must have evicted down under 256KB.
|
|
143
|
-
expect(peek
|
|
144
|
-
expect(peek
|
|
160
|
+
expect(peek.totalSizeBytes).toBeLessThanOrEqual(256 * 1024);
|
|
161
|
+
expect(peek.ringLength).toBeLessThan(60);
|
|
145
162
|
});
|
|
146
163
|
|
|
147
|
-
test("evicts past the 30s age cap",
|
|
164
|
+
test("evicts past the 30s age cap", () => {
|
|
148
165
|
const originalNow = Date.now;
|
|
149
166
|
let fakeNow = 1_000_000;
|
|
150
167
|
Date.now = () => fakeNow;
|
|
@@ -157,11 +174,11 @@ describe("conversation-stream-state", () => {
|
|
|
157
174
|
fakeNow = 1_000_000 + 31_000;
|
|
158
175
|
stampAndBuffer(mkEvent()); // triggers eviction sweep on push
|
|
159
176
|
|
|
160
|
-
const peek = _peekStreamForTesting(
|
|
177
|
+
const peek = _peekStreamForTesting();
|
|
161
178
|
// First event is now > 30s old → evicted. Second + third remain.
|
|
162
|
-
expect(peek
|
|
163
|
-
expect(peek
|
|
164
|
-
expect(peek
|
|
179
|
+
expect(peek.ringLength).toBe(2);
|
|
180
|
+
expect(peek.oldestSeq).toBe(2);
|
|
181
|
+
expect(peek.newestSeq).toBe(3);
|
|
165
182
|
} finally {
|
|
166
183
|
Date.now = originalNow;
|
|
167
184
|
}
|
|
@@ -172,7 +189,7 @@ describe("conversation-stream-state", () => {
|
|
|
172
189
|
test("returns events with seq > lastSeenSeq in order", () => {
|
|
173
190
|
const events = Array.from({ length: 5 }, () => mkEvent());
|
|
174
191
|
events.forEach((e) => stampAndBuffer(e));
|
|
175
|
-
const replay = getReplayWindow(
|
|
192
|
+
const replay = getReplayWindow(2);
|
|
176
193
|
expect(replay).not.toBeNull();
|
|
177
194
|
expect(replay!.map((e) => e.seq)).toEqual([3, 4, 5]);
|
|
178
195
|
});
|
|
@@ -180,22 +197,22 @@ describe("conversation-stream-state", () => {
|
|
|
180
197
|
test("returns empty array when lastSeenSeq is current (nothing to replay)", () => {
|
|
181
198
|
stampAndBuffer(mkEvent());
|
|
182
199
|
stampAndBuffer(mkEvent());
|
|
183
|
-
const replay = getReplayWindow(
|
|
200
|
+
const replay = getReplayWindow(2);
|
|
184
201
|
expect(replay).toEqual([]);
|
|
185
202
|
});
|
|
186
203
|
|
|
187
204
|
test("returns null when lastSeenSeq is older than oldest buffered entry", () => {
|
|
188
205
|
// Force eviction by pushing past the count cap.
|
|
189
206
|
for (let i = 0; i < 250; i++) stampAndBuffer(mkEvent());
|
|
190
|
-
const peek = _peekStreamForTesting(
|
|
191
|
-
expect(peek
|
|
207
|
+
const peek = _peekStreamForTesting();
|
|
208
|
+
expect(peek.oldestSeq).toBe(51);
|
|
192
209
|
// Client claims to have last seen seq=10 — that's far below oldest.
|
|
193
|
-
const replay = getReplayWindow(
|
|
210
|
+
const replay = getReplayWindow(10);
|
|
194
211
|
expect(replay).toBeNull();
|
|
195
212
|
});
|
|
196
213
|
|
|
197
|
-
test("returns empty array
|
|
198
|
-
const replay = getReplayWindow(
|
|
214
|
+
test("returns empty array when the ring is empty", () => {
|
|
215
|
+
const replay = getReplayWindow(0);
|
|
199
216
|
expect(replay).toEqual([]);
|
|
200
217
|
});
|
|
201
218
|
|
|
@@ -204,12 +221,12 @@ describe("conversation-stream-state", () => {
|
|
|
204
221
|
stampAndBuffer(mkEvent()); // seq 2
|
|
205
222
|
stampAndBuffer(mkEvent()); // seq 3
|
|
206
223
|
// Client saw nothing → lastSeenSeq=0, oldest=1, replay [1,2,3].
|
|
207
|
-
const replay = getReplayWindow(
|
|
224
|
+
const replay = getReplayWindow(0);
|
|
208
225
|
expect(replay).not.toBeNull();
|
|
209
226
|
expect(replay!.map((e) => e.seq)).toEqual([1, 2, 3]);
|
|
210
227
|
});
|
|
211
228
|
|
|
212
|
-
test("evicts age-expired entries at read time on idle stream", () => {
|
|
229
|
+
test("evicts age-expired entries at read time on an idle stream", () => {
|
|
213
230
|
const originalNow = Date.now;
|
|
214
231
|
let fakeNow = 5_000_000;
|
|
215
232
|
Date.now = () => fakeNow;
|
|
@@ -223,15 +240,14 @@ describe("conversation-stream-state", () => {
|
|
|
223
240
|
|
|
224
241
|
// Eviction has not run since the last write -- the buffer still
|
|
225
242
|
// physically holds [1, 2]. getReplayWindow must sweep first.
|
|
226
|
-
const replay = getReplayWindow(
|
|
243
|
+
const replay = getReplayWindow(0);
|
|
227
244
|
|
|
228
245
|
// Both events were past their TTL, so eviction drains the ring
|
|
229
|
-
// and the call returns [] (no replay possible, no snapshot
|
|
230
|
-
//
|
|
231
|
-
//
|
|
246
|
+
// and the call returns [] (no replay possible, no snapshot needed
|
|
247
|
+
// either -- client claims they saw nothing and there is nothing
|
|
248
|
+
// left).
|
|
232
249
|
expect(replay).toEqual([]);
|
|
233
|
-
|
|
234
|
-
expect(_peekStreamForTesting(CONV)).toBeNull();
|
|
250
|
+
expect(_peekStreamForTesting().ringLength).toBe(0);
|
|
235
251
|
} finally {
|
|
236
252
|
Date.now = originalNow;
|
|
237
253
|
}
|
|
@@ -252,13 +268,13 @@ describe("conversation-stream-state", () => {
|
|
|
252
268
|
stampAndBuffer(mkEvent()); // seq 3
|
|
253
269
|
// After this write, evict() already ran and dropped the stale
|
|
254
270
|
// entries from the write path. Verify that.
|
|
255
|
-
const peek = _peekStreamForTesting(
|
|
256
|
-
expect(peek
|
|
257
|
-
expect(peek
|
|
271
|
+
const peek = _peekStreamForTesting();
|
|
272
|
+
expect(peek.ringLength).toBe(1);
|
|
273
|
+
expect(peek.oldestSeq).toBe(3);
|
|
258
274
|
|
|
259
275
|
// Client reconnects claiming lastSeenSeq=1. Oldest buffered is
|
|
260
276
|
// 3, so 1 < 3 - 1 = 2 -> snapshot fallback (null).
|
|
261
|
-
const replay = getReplayWindow(
|
|
277
|
+
const replay = getReplayWindow(1);
|
|
262
278
|
expect(replay).toBeNull();
|
|
263
279
|
} finally {
|
|
264
280
|
Date.now = originalNow;
|
|
@@ -266,6 +282,42 @@ describe("conversation-stream-state", () => {
|
|
|
266
282
|
});
|
|
267
283
|
});
|
|
268
284
|
|
|
285
|
+
describe("getReplayWindow — conversation filter", () => {
|
|
286
|
+
test("restricts replay to a single conversation when conversationId is given", () => {
|
|
287
|
+
/**
|
|
288
|
+
* A scoped subscription only delivers its own conversation live, so
|
|
289
|
+
* replay must not push other conversations' buffered events.
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
// GIVEN events interleaved across two conversations in the global ring
|
|
293
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_a" })); // seq 1
|
|
294
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_b" })); // seq 2
|
|
295
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_a" })); // seq 3
|
|
296
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_b" })); // seq 4
|
|
297
|
+
|
|
298
|
+
// WHEN replay is scoped to conv_a
|
|
299
|
+
const replay = getReplayWindow(0, undefined, "conv_a");
|
|
300
|
+
|
|
301
|
+
// THEN only conv_a's events return, still in global seq order
|
|
302
|
+
expect(replay!.map((e) => e.seq)).toEqual([1, 3]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("unfiltered replay returns every conversation's events in seq order", () => {
|
|
306
|
+
/** The unfiltered (assistant-wide) stream resumes the whole ring. */
|
|
307
|
+
|
|
308
|
+
// GIVEN events across two conversations
|
|
309
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_a" })); // seq 1
|
|
310
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_b" })); // seq 2
|
|
311
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_a" })); // seq 3
|
|
312
|
+
|
|
313
|
+
// WHEN replay is requested without a conversation filter
|
|
314
|
+
const replay = getReplayWindow(0);
|
|
315
|
+
|
|
316
|
+
// THEN all events return in one contiguous global seq order
|
|
317
|
+
expect(replay!.map((e) => e.seq)).toEqual([1, 2, 3]);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
269
321
|
describe("getReplayWindow — targeting filter", () => {
|
|
270
322
|
const MACOS_CLIENT: ReplaySubscriber = {
|
|
271
323
|
type: "client",
|
|
@@ -298,9 +350,9 @@ describe("conversation-stream-state", () => {
|
|
|
298
350
|
stampAndBuffer(mkEvent());
|
|
299
351
|
|
|
300
352
|
// WHEN each subscriber type requests replay
|
|
301
|
-
const macReplay = getReplayWindow(
|
|
302
|
-
const webReplay = getReplayWindow(
|
|
303
|
-
const procReplay = getReplayWindow(
|
|
353
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
354
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
355
|
+
const procReplay = getReplayWindow(0, PROCESS_SUB);
|
|
304
356
|
|
|
305
357
|
// THEN all see both events
|
|
306
358
|
expect(macReplay!.map((e) => e.seq)).toEqual([1, 2]);
|
|
@@ -318,9 +370,9 @@ describe("conversation-stream-state", () => {
|
|
|
318
370
|
});
|
|
319
371
|
|
|
320
372
|
// WHEN each subscriber requests replay
|
|
321
|
-
const macReplay = getReplayWindow(
|
|
322
|
-
const webReplay = getReplayWindow(
|
|
323
|
-
const procReplay = getReplayWindow(
|
|
373
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
374
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
375
|
+
const procReplay = getReplayWindow(0, PROCESS_SUB);
|
|
324
376
|
|
|
325
377
|
// THEN macOS sees both; web and process see only the untargeted event
|
|
326
378
|
expect(macReplay!.map((e) => e.seq)).toEqual([1, 2]);
|
|
@@ -337,9 +389,9 @@ describe("conversation-stream-state", () => {
|
|
|
337
389
|
});
|
|
338
390
|
|
|
339
391
|
// WHEN macOS and chrome-extension request replay
|
|
340
|
-
const macReplay = getReplayWindow(
|
|
341
|
-
const extReplay = getReplayWindow(
|
|
342
|
-
const webReplay = getReplayWindow(
|
|
392
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
393
|
+
const extReplay = getReplayWindow(0, CHROME_EXT_CLIENT);
|
|
394
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
343
395
|
|
|
344
396
|
// THEN both capable clients see it; web does not
|
|
345
397
|
expect(macReplay!.map((e) => e.seq)).toEqual([1]);
|
|
@@ -356,9 +408,9 @@ describe("conversation-stream-state", () => {
|
|
|
356
408
|
});
|
|
357
409
|
|
|
358
410
|
// WHEN different clients request replay
|
|
359
|
-
const macReplay = getReplayWindow(
|
|
360
|
-
const webReplay = getReplayWindow(
|
|
361
|
-
const procReplay = getReplayWindow(
|
|
411
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
412
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
413
|
+
const procReplay = getReplayWindow(0, PROCESS_SUB);
|
|
362
414
|
|
|
363
415
|
// THEN only the named client receives it
|
|
364
416
|
expect(macReplay!.map((e) => e.seq)).toEqual([1]);
|
|
@@ -378,8 +430,8 @@ describe("conversation-stream-state", () => {
|
|
|
378
430
|
});
|
|
379
431
|
|
|
380
432
|
// WHEN the named client (without the capability) and macOS request replay
|
|
381
|
-
const webReplay = getReplayWindow(
|
|
382
|
-
const macReplay = getReplayWindow(
|
|
433
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
434
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
383
435
|
|
|
384
436
|
// THEN neither receives it — web-1 lacks the capability, mac-1 isn't the target
|
|
385
437
|
expect(webReplay).toEqual([]);
|
|
@@ -395,9 +447,9 @@ describe("conversation-stream-state", () => {
|
|
|
395
447
|
});
|
|
396
448
|
|
|
397
449
|
// WHEN web-1 and mac-1 request replay
|
|
398
|
-
const webReplay = getReplayWindow(
|
|
399
|
-
const macReplay = getReplayWindow(
|
|
400
|
-
const procReplay = getReplayWindow(
|
|
450
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
451
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
452
|
+
const procReplay = getReplayWindow(0, PROCESS_SUB);
|
|
401
453
|
|
|
402
454
|
// THEN web-1 is suppressed; mac-1 and process subscribers see it
|
|
403
455
|
expect(webReplay).toEqual([]);
|
|
@@ -414,9 +466,9 @@ describe("conversation-stream-state", () => {
|
|
|
414
466
|
});
|
|
415
467
|
|
|
416
468
|
// WHEN different subscribers request replay
|
|
417
|
-
const macReplay = getReplayWindow(
|
|
418
|
-
const webReplay = getReplayWindow(
|
|
419
|
-
const procReplay = getReplayWindow(
|
|
469
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
470
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
471
|
+
const procReplay = getReplayWindow(0, PROCESS_SUB);
|
|
420
472
|
|
|
421
473
|
// THEN only the macos client receives it
|
|
422
474
|
expect(macReplay!.map((e) => e.seq)).toEqual([1]);
|
|
@@ -441,8 +493,8 @@ describe("conversation-stream-state", () => {
|
|
|
441
493
|
stampAndBuffer(mkEvent()); // seq 4: untargeted
|
|
442
494
|
|
|
443
495
|
// WHEN each subscriber requests replay from seq 0
|
|
444
|
-
const macReplay = getReplayWindow(
|
|
445
|
-
const webReplay = getReplayWindow(
|
|
496
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT);
|
|
497
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT);
|
|
446
498
|
|
|
447
499
|
// THEN macOS sees all four; web sees 1 + 4 (not 2=no capability, not 3=excluded)
|
|
448
500
|
expect(macReplay!.map((e) => e.seq)).toEqual([1, 2, 3, 4]);
|
|
@@ -450,7 +502,7 @@ describe("conversation-stream-state", () => {
|
|
|
450
502
|
});
|
|
451
503
|
|
|
452
504
|
test("no subscriber argument returns all entries unfiltered", () => {
|
|
453
|
-
/**
|
|
505
|
+
/** Omitting subscriber skips targeting filtering. */
|
|
454
506
|
|
|
455
507
|
// GIVEN targeted and untargeted events
|
|
456
508
|
stampAndBuffer(mkEvent());
|
|
@@ -459,26 +511,135 @@ describe("conversation-stream-state", () => {
|
|
|
459
511
|
});
|
|
460
512
|
|
|
461
513
|
// WHEN replay is requested without a subscriber
|
|
462
|
-
const replay = getReplayWindow(
|
|
514
|
+
const replay = getReplayWindow(0);
|
|
463
515
|
|
|
464
516
|
// THEN all events are returned
|
|
465
517
|
expect(replay!.map((e) => e.seq)).toEqual([1, 2]);
|
|
466
518
|
});
|
|
519
|
+
|
|
520
|
+
test("subscriber and conversation filters compose", () => {
|
|
521
|
+
/**
|
|
522
|
+
* When both filters are supplied, an entry must satisfy targeting
|
|
523
|
+
* AND belong to the requested conversation.
|
|
524
|
+
*/
|
|
525
|
+
|
|
526
|
+
// GIVEN bash-targeted events across two conversations
|
|
527
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_a" }), {
|
|
528
|
+
targeting: { targetCapability: "host_bash" },
|
|
529
|
+
}); // seq 1
|
|
530
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_b" }), {
|
|
531
|
+
targeting: { targetCapability: "host_bash" },
|
|
532
|
+
}); // seq 2
|
|
533
|
+
stampAndBuffer(mkEvent({ conversationId: "conv_a" })); // seq 3 untargeted
|
|
534
|
+
|
|
535
|
+
// WHEN a web client (no host_bash) replays scoped to conv_a
|
|
536
|
+
const webReplay = getReplayWindow(0, WEB_CLIENT, "conv_a");
|
|
537
|
+
// AND macOS replays scoped to conv_a
|
|
538
|
+
const macReplay = getReplayWindow(0, MACOS_CLIENT, "conv_a");
|
|
539
|
+
|
|
540
|
+
// THEN web sees only conv_a's untargeted event; macOS sees both conv_a entries
|
|
541
|
+
expect(webReplay!.map((e) => e.seq)).toEqual([3]);
|
|
542
|
+
expect(macReplay!.map((e) => e.seq)).toEqual([1, 3]);
|
|
543
|
+
});
|
|
467
544
|
});
|
|
468
545
|
|
|
469
|
-
describe("
|
|
470
|
-
test("
|
|
471
|
-
|
|
546
|
+
describe("getCurrentSeq", () => {
|
|
547
|
+
test("is 0 before anything is stamped", () => {
|
|
548
|
+
expect(getCurrentSeq()).toBe(0);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("reports the seq just assigned by stampAndBuffer", () => {
|
|
552
|
+
const a = mkEvent();
|
|
553
|
+
stampAndBuffer(a);
|
|
554
|
+
expect(a.seq).toBe(1);
|
|
555
|
+
expect(getCurrentSeq()).toBe(1);
|
|
556
|
+
|
|
557
|
+
const b = mkEvent();
|
|
558
|
+
stampAndBuffer(b);
|
|
559
|
+
expect(b.seq).toBe(2);
|
|
560
|
+
expect(getCurrentSeq()).toBe(2);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("unscoped (unstamped) events do not advance it", () => {
|
|
472
564
|
stampAndBuffer(mkEvent());
|
|
473
|
-
|
|
565
|
+
// An event with no conversationId is never stamped.
|
|
566
|
+
stampAndBuffer(mkEvent({ conversationId: undefined }));
|
|
567
|
+
expect(getCurrentSeq()).toBe(1);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
474
570
|
|
|
475
|
-
|
|
571
|
+
describe("persisted seq", () => {
|
|
572
|
+
test("getPersistedSeq is null for an unknown conversation", () => {
|
|
573
|
+
expect(getPersistedSeq("conv_unknown")).toBeNull();
|
|
574
|
+
});
|
|
476
575
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
576
|
+
test("records and retrieves a per-conversation value", () => {
|
|
577
|
+
recordPersistedSeq("conv_a", 7);
|
|
578
|
+
expect(getPersistedSeq("conv_a")).toBe(7);
|
|
579
|
+
expect(getPersistedSeq("conv_b")).toBeNull();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("tracks conversations independently", () => {
|
|
583
|
+
recordPersistedSeq("conv_a", 3);
|
|
584
|
+
recordPersistedSeq("conv_b", 9);
|
|
585
|
+
expect(getPersistedSeq("conv_a")).toBe(3);
|
|
586
|
+
expect(getPersistedSeq("conv_b")).toBe(9);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("advances monotonically and never regresses", () => {
|
|
590
|
+
recordPersistedSeq("conv_a", 5);
|
|
591
|
+
recordPersistedSeq("conv_a", 12);
|
|
592
|
+
expect(getPersistedSeq("conv_a")).toBe(12);
|
|
593
|
+
|
|
594
|
+
// A lower seq (e.g. an out-of-order async commit) is clamped.
|
|
595
|
+
recordPersistedSeq("conv_a", 8);
|
|
596
|
+
expect(getPersistedSeq("conv_a")).toBe(12);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("ignores non-positive and non-finite seq values", () => {
|
|
600
|
+
recordPersistedSeq("conv_a", 0);
|
|
601
|
+
recordPersistedSeq("conv_a", -3);
|
|
602
|
+
recordPersistedSeq("conv_a", Number.NaN);
|
|
603
|
+
recordPersistedSeq("conv_a", Number.POSITIVE_INFINITY);
|
|
604
|
+
expect(getPersistedSeq("conv_a")).toBeNull();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("is cleared by reset", () => {
|
|
608
|
+
recordPersistedSeq("conv_a", 4);
|
|
609
|
+
_resetStreamStateForTesting();
|
|
610
|
+
expect(getPersistedSeq("conv_a")).toBeNull();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("evicts the least-recently-recorded conversation past the cap", () => {
|
|
614
|
+
// The map is LRU-bounded at 1024 conversations. Fill to the cap,
|
|
615
|
+
// then one more insert evicts the oldest key.
|
|
616
|
+
const CAP = 1024;
|
|
617
|
+
for (let i = 0; i < CAP; i++) {
|
|
618
|
+
recordPersistedSeq(`conv_${i}`, i + 1);
|
|
619
|
+
}
|
|
620
|
+
// All present at the cap.
|
|
621
|
+
expect(getPersistedSeq("conv_0")).toBe(1);
|
|
622
|
+
expect(getPersistedSeq(`conv_${CAP - 1}`)).toBe(CAP);
|
|
623
|
+
|
|
624
|
+
// One more distinct conversation evicts the oldest (conv_0).
|
|
625
|
+
recordPersistedSeq("conv_overflow", 9999);
|
|
626
|
+
expect(getPersistedSeq("conv_0")).toBeNull();
|
|
627
|
+
expect(getPersistedSeq("conv_1")).toBe(2);
|
|
628
|
+
expect(getPersistedSeq("conv_overflow")).toBe(9999);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("re-recording refreshes recency so a kept key is not evicted first", () => {
|
|
632
|
+
const CAP = 1024;
|
|
633
|
+
for (let i = 0; i < CAP; i++) {
|
|
634
|
+
recordPersistedSeq(`conv_${i}`, i + 1);
|
|
635
|
+
}
|
|
636
|
+
// Touch the oldest key so it moves to the most-recent end.
|
|
637
|
+
recordPersistedSeq("conv_0", 5000);
|
|
638
|
+
|
|
639
|
+
// The next insert now evicts conv_1 (the new oldest), not conv_0.
|
|
640
|
+
recordPersistedSeq("conv_overflow", 9999);
|
|
641
|
+
expect(getPersistedSeq("conv_0")).toBe(5000);
|
|
642
|
+
expect(getPersistedSeq("conv_1")).toBeNull();
|
|
482
643
|
});
|
|
483
644
|
});
|
|
484
645
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../util/logger.js", () => ({
|
|
4
|
+
getLogger: () =>
|
|
5
|
+
new Proxy({} as Record<string, unknown>, {
|
|
6
|
+
get: () => () => {},
|
|
7
|
+
}),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
let collectUsageData = true;
|
|
11
|
+
|
|
12
|
+
mock.module("../config/loader.js", () => ({
|
|
13
|
+
getConfig: () => ({
|
|
14
|
+
ui: {},
|
|
15
|
+
model: "test",
|
|
16
|
+
provider: "test",
|
|
17
|
+
memory: { enabled: false },
|
|
18
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
19
|
+
secretDetection: { enabled: false },
|
|
20
|
+
collectUsageData,
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type AuthFallbackCount,
|
|
26
|
+
queryUnreportedAuthFallbackEvents,
|
|
27
|
+
recordAuthFallbackCounts,
|
|
28
|
+
} from "../memory/auth-fallback-events-store.js";
|
|
29
|
+
import { getDb } from "../memory/db-connection.js";
|
|
30
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
31
|
+
import { authFallbackEvents } from "../memory/schema.js";
|
|
32
|
+
|
|
33
|
+
initializeDb();
|
|
34
|
+
|
|
35
|
+
function resetTable(): void {
|
|
36
|
+
getDb().delete(authFallbackEvents).run();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SAMPLE: AuthFallbackCount[] = [
|
|
40
|
+
{
|
|
41
|
+
guard: "edge",
|
|
42
|
+
path: "/v1/messages",
|
|
43
|
+
failureKind: "missing_authorization",
|
|
44
|
+
count: 7,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
guard: "edge-scoped",
|
|
48
|
+
path: "/v1/files",
|
|
49
|
+
failureKind: "insufficient_scope",
|
|
50
|
+
count: 2,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
describe("auth-fallback-events-store", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
collectUsageData = true;
|
|
57
|
+
resetTable();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("records one row per count entry and they are queryable", () => {
|
|
61
|
+
const recorded = recordAuthFallbackCounts(1000, 2000, SAMPLE);
|
|
62
|
+
expect(recorded).toBe(2);
|
|
63
|
+
|
|
64
|
+
const rows = queryUnreportedAuthFallbackEvents(0, undefined, 100);
|
|
65
|
+
expect(rows.length).toBe(2);
|
|
66
|
+
const byGuard = Object.fromEntries(rows.map((r) => [r.guard, r]));
|
|
67
|
+
expect(byGuard["edge"]).toMatchObject({
|
|
68
|
+
path: "/v1/messages",
|
|
69
|
+
failureKind: "missing_authorization",
|
|
70
|
+
count: 7,
|
|
71
|
+
windowStart: 1000,
|
|
72
|
+
windowEnd: 2000,
|
|
73
|
+
});
|
|
74
|
+
expect(byGuard["edge-scoped"]).toMatchObject({
|
|
75
|
+
path: "/v1/files",
|
|
76
|
+
failureKind: "insufficient_scope",
|
|
77
|
+
count: 2,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("honors the collectUsageData opt-out (records nothing)", () => {
|
|
82
|
+
collectUsageData = false;
|
|
83
|
+
const recorded = recordAuthFallbackCounts(1000, 2000, SAMPLE);
|
|
84
|
+
expect(recorded).toBe(0);
|
|
85
|
+
expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("empty counts batch is a no-op", () => {
|
|
89
|
+
expect(recordAuthFallbackCounts(1000, 2000, [])).toBe(0);
|
|
90
|
+
expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("query advances past the compound (createdAt, id) cursor", () => {
|
|
94
|
+
recordAuthFallbackCounts(1000, 2000, SAMPLE);
|
|
95
|
+
const all = queryUnreportedAuthFallbackEvents(0, undefined, 100);
|
|
96
|
+
expect(all.length).toBe(2);
|
|
97
|
+
|
|
98
|
+
// All rows share the same createdAt (one insert batch). Paginating with a
|
|
99
|
+
// limit of 1 must use the id tiebreaker to make forward progress, not loop.
|
|
100
|
+
const first = queryUnreportedAuthFallbackEvents(0, undefined, 1);
|
|
101
|
+
expect(first.length).toBe(1);
|
|
102
|
+
const second = queryUnreportedAuthFallbackEvents(
|
|
103
|
+
first[0].createdAt,
|
|
104
|
+
first[0].id,
|
|
105
|
+
1,
|
|
106
|
+
);
|
|
107
|
+
expect(second.length).toBe(1);
|
|
108
|
+
expect(second[0].id).not.toBe(first[0].id);
|
|
109
|
+
|
|
110
|
+
// Cursor past the last row returns nothing.
|
|
111
|
+
const last = all[all.length - 1];
|
|
112
|
+
expect(
|
|
113
|
+
queryUnreportedAuthFallbackEvents(last.createdAt, last.id, 100).length,
|
|
114
|
+
).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
});
|