@vellumai/assistant 0.8.10 → 0.8.11-staging.1
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/bun.lock +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -143,19 +143,31 @@ mock.module("../memory/onboarding-events-store.js", () => ({
|
|
|
143
143
|
queryUnreportedOnboardingEvents: mockQueryUnreportedOnboardingEvents,
|
|
144
144
|
}));
|
|
145
145
|
|
|
146
|
-
// The auth-fallback
|
|
147
|
-
// DB-backed tests, and Bun's `mock.module`
|
|
148
|
-
// here would leak into those tests when
|
|
149
|
-
// real DB instead so every
|
|
146
|
+
// The auth-fallback, tool-executed, and skill-loaded stores are intentionally
|
|
147
|
+
// NOT mocked — they have their own DB-backed tests, and Bun's `mock.module`
|
|
148
|
+
// is process-global, so mocking them here would leak into those tests when
|
|
149
|
+
// files share an invocation. We seed the real DB instead so every test stays
|
|
150
|
+
// order-independent.
|
|
150
151
|
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
152
153
|
// Production import (after mocks)
|
|
153
154
|
// ---------------------------------------------------------------------------
|
|
154
155
|
|
|
156
|
+
import {
|
|
157
|
+
seedToolInvocation as seedToolInvocationRow,
|
|
158
|
+
TOOL_INVOCATION_PII_SENTINEL as TOOL_PII_SENTINEL,
|
|
159
|
+
type ToolInvocationSeedSpec,
|
|
160
|
+
} from "../__tests__/test-support/tool-invocation-seed.js";
|
|
155
161
|
import { recordAuthFallbackCounts } from "../memory/auth-fallback-events-store.js";
|
|
156
162
|
import { getDb } from "../memory/db-connection.js";
|
|
157
163
|
import { initializeDb } from "../memory/db-init.js";
|
|
158
|
-
import {
|
|
164
|
+
import {
|
|
165
|
+
authFallbackEvents,
|
|
166
|
+
conversations,
|
|
167
|
+
skillLoadedEvents,
|
|
168
|
+
toolInvocations,
|
|
169
|
+
} from "../memory/schema.js";
|
|
170
|
+
import { recordSkillLoadedEvent } from "../memory/skill-loaded-events-store.js";
|
|
159
171
|
import type { UsageEvent } from "../usage/types.js";
|
|
160
172
|
import {
|
|
161
173
|
ACTIVATION_FUNNEL_VERSION,
|
|
@@ -238,6 +250,36 @@ function makeOnboardingEvent(
|
|
|
238
250
|
};
|
|
239
251
|
}
|
|
240
252
|
|
|
253
|
+
const TOOL_CONVERSATION_ID = "conv-reporter-tool-executed";
|
|
254
|
+
|
|
255
|
+
function seedToolInvocation(
|
|
256
|
+
spec: Omit<ToolInvocationSeedSpec, "conversationId">,
|
|
257
|
+
): void {
|
|
258
|
+
seedToolInvocationRow(
|
|
259
|
+
{ db: getDb(), conversations, toolInvocations },
|
|
260
|
+
{ ...spec, conversationId: TOOL_CONVERSATION_ID },
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Replace the default null-returning checkpoint mocks with a Map-backed
|
|
266
|
+
* implementation so values persisted by the reporter (e.g. the
|
|
267
|
+
* construction-time tool_executed watermark init) are visible to later
|
|
268
|
+
* reads within the same test.
|
|
269
|
+
*/
|
|
270
|
+
function useStatefulCheckpoints(
|
|
271
|
+
seed: Record<string, string> = {},
|
|
272
|
+
): Map<string, string> {
|
|
273
|
+
const checkpoints = new Map(Object.entries(seed));
|
|
274
|
+
mockGetMemoryCheckpoint.mockImplementation(
|
|
275
|
+
(key) => checkpoints.get(key) ?? null,
|
|
276
|
+
);
|
|
277
|
+
mockSetMemoryCheckpoint.mockImplementation((key, value) => {
|
|
278
|
+
checkpoints.set(key, value);
|
|
279
|
+
});
|
|
280
|
+
return checkpoints;
|
|
281
|
+
}
|
|
282
|
+
|
|
241
283
|
const originalFetch = globalThis.fetch;
|
|
242
284
|
let mockFetch: ReturnType<typeof mock>;
|
|
243
285
|
|
|
@@ -258,6 +300,8 @@ beforeEach(() => {
|
|
|
258
300
|
mockQueryUnreportedOnboardingEvents.mockReset();
|
|
259
301
|
mockQueryUnreportedOnboardingEvents.mockReturnValue([]);
|
|
260
302
|
getDb().delete(authFallbackEvents).run();
|
|
303
|
+
getDb().delete(toolInvocations).run();
|
|
304
|
+
getDb().delete(skillLoadedEvents).run();
|
|
261
305
|
mockPlatformClient = null;
|
|
262
306
|
mockGetPlatformBaseUrl.mockReset();
|
|
263
307
|
mockGetDeviceId.mockReset();
|
|
@@ -961,22 +1005,38 @@ describe("UsageTelemetryReporter", () => {
|
|
|
961
1005
|
);
|
|
962
1006
|
|
|
963
1007
|
const reporter = new UsageTelemetryReporter();
|
|
1008
|
+
// Construction initializes the absent tool_executed watermark; clear that
|
|
1009
|
+
// call so the count below covers only the flush's advancement.
|
|
1010
|
+
mockSetMemoryCheckpoint.mockClear();
|
|
964
1011
|
await reporter.flush();
|
|
965
1012
|
|
|
966
1013
|
// No HTTP call should have been made
|
|
967
1014
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
968
1015
|
|
|
969
|
-
// All
|
|
970
|
-
//
|
|
971
|
-
|
|
1016
|
+
// All 7 timestamp watermarks should have been advanced, and all 7 ID
|
|
1017
|
+
// watermarks pinned to the high-sorting sentinel (a truthy value keeps
|
|
1018
|
+
// the compound-cursor branch active while closing its same-millisecond
|
|
1019
|
+
// arm against opt-out rows).
|
|
1020
|
+
expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(14);
|
|
972
1021
|
|
|
973
1022
|
const calls = mockSetMemoryCheckpoint.mock.calls;
|
|
974
1023
|
const keys = calls.map((c) => c[0]);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1024
|
+
const eventTypes = [
|
|
1025
|
+
"usage",
|
|
1026
|
+
"turns",
|
|
1027
|
+
"lifecycle",
|
|
1028
|
+
"onboarding",
|
|
1029
|
+
"auth_fallback",
|
|
1030
|
+
"tool_executed",
|
|
1031
|
+
"skill_loaded",
|
|
1032
|
+
];
|
|
1033
|
+
for (const eventType of eventTypes) {
|
|
1034
|
+
expect(keys).toContain(`telemetry:${eventType}:last_reported_at`);
|
|
1035
|
+
const idCall = calls.find(
|
|
1036
|
+
(c) => c[0] === `telemetry:${eventType}:last_reported_id`,
|
|
1037
|
+
);
|
|
1038
|
+
expect(idCall?.[1]).toBe("ffffffff-ffff-ffff-ffff-ffffffffffff");
|
|
1039
|
+
}
|
|
980
1040
|
});
|
|
981
1041
|
|
|
982
1042
|
test("events sent normally after re-enabling collectUsageData", async () => {
|
|
@@ -1338,4 +1398,473 @@ describe("UsageTelemetryReporter", () => {
|
|
|
1338
1398
|
expect(e.completed_at).toBeUndefined();
|
|
1339
1399
|
expect(e.funnel_version).toBeUndefined();
|
|
1340
1400
|
});
|
|
1401
|
+
|
|
1402
|
+
// -------------------------------------------------------------------------
|
|
1403
|
+
// Tool-executed events
|
|
1404
|
+
// -------------------------------------------------------------------------
|
|
1405
|
+
|
|
1406
|
+
test("tool_executed events project decision to status and never carry raw args/results", async () => {
|
|
1407
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1408
|
+
seedToolInvocation({
|
|
1409
|
+
id: "ti-ok",
|
|
1410
|
+
createdAt: 1700000001000,
|
|
1411
|
+
toolName: "calendar_list_events",
|
|
1412
|
+
decision: "allow",
|
|
1413
|
+
durationMs: 12,
|
|
1414
|
+
argBytes: 42,
|
|
1415
|
+
resultBytes: 9001,
|
|
1416
|
+
provider: "anthropic",
|
|
1417
|
+
model: "model-a",
|
|
1418
|
+
inferenceProfile: "balanced",
|
|
1419
|
+
inferenceProfileSource: "active",
|
|
1420
|
+
});
|
|
1421
|
+
// Errored invocation without LLM attribution — the attribution columns
|
|
1422
|
+
// are null and must pass through as null.
|
|
1423
|
+
seedToolInvocation({
|
|
1424
|
+
id: "ti-err",
|
|
1425
|
+
createdAt: 1700000002000,
|
|
1426
|
+
toolName: "web_search",
|
|
1427
|
+
decision: "error",
|
|
1428
|
+
durationMs: 7,
|
|
1429
|
+
argBytes: 17,
|
|
1430
|
+
resultBytes: 33,
|
|
1431
|
+
});
|
|
1432
|
+
// Permission-denied rows are filtered in the store and must never ship.
|
|
1433
|
+
seedToolInvocation({
|
|
1434
|
+
id: "ti-denied",
|
|
1435
|
+
createdAt: 1700000003000,
|
|
1436
|
+
decision: "denied",
|
|
1437
|
+
});
|
|
1438
|
+
mockFetch.mockImplementation(() =>
|
|
1439
|
+
Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
const reporter = new UsageTelemetryReporter();
|
|
1443
|
+
await reporter.flush();
|
|
1444
|
+
|
|
1445
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1446
|
+
const rawBody = (mockFetch.mock.calls[0] as [string, RequestInit])[1]
|
|
1447
|
+
.body as string;
|
|
1448
|
+
// ToS/PII contract: raw tool args/outputs never leave the device.
|
|
1449
|
+
expect(rawBody).not.toContain(TOOL_PII_SENTINEL);
|
|
1450
|
+
|
|
1451
|
+
const body = JSON.parse(rawBody);
|
|
1452
|
+
expect(body.events.length).toBe(2);
|
|
1453
|
+
|
|
1454
|
+
const byId: Record<string, Record<string, unknown>> = {};
|
|
1455
|
+
for (const e of body.events as Array<{ daemon_event_id: string }>) {
|
|
1456
|
+
byId[e.daemon_event_id] = e;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
expect(byId["ti-ok"]).toEqual({
|
|
1460
|
+
type: "tool_executed",
|
|
1461
|
+
daemon_event_id: "ti-ok",
|
|
1462
|
+
recorded_at: 1700000001000,
|
|
1463
|
+
tool_name: "calendar_list_events",
|
|
1464
|
+
status: "fulfilled",
|
|
1465
|
+
duration_ms: 12,
|
|
1466
|
+
arg_bytes: 42,
|
|
1467
|
+
result_bytes: 9001,
|
|
1468
|
+
conversation_id: TOOL_CONVERSATION_ID,
|
|
1469
|
+
provider: "anthropic",
|
|
1470
|
+
model: "model-a",
|
|
1471
|
+
inference_profile: "balanced",
|
|
1472
|
+
inference_profile_source: "active",
|
|
1473
|
+
assistant_version: "1.2.3-test",
|
|
1474
|
+
});
|
|
1475
|
+
expect(byId["ti-err"]).toMatchObject({
|
|
1476
|
+
type: "tool_executed",
|
|
1477
|
+
tool_name: "web_search",
|
|
1478
|
+
status: "errored",
|
|
1479
|
+
duration_ms: 7,
|
|
1480
|
+
arg_bytes: 17,
|
|
1481
|
+
result_bytes: 33,
|
|
1482
|
+
provider: null,
|
|
1483
|
+
model: null,
|
|
1484
|
+
inference_profile: null,
|
|
1485
|
+
inference_profile_source: null,
|
|
1486
|
+
});
|
|
1487
|
+
expect(byId["ti-denied"]).toBeUndefined();
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
test("tool_executed watermark advances to the last reported row on success", async () => {
|
|
1491
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1492
|
+
seedToolInvocation({ id: "ti-w1", createdAt: 1700000001000 });
|
|
1493
|
+
seedToolInvocation({ id: "ti-w2", createdAt: 1700000002000 });
|
|
1494
|
+
mockFetch.mockImplementation(() =>
|
|
1495
|
+
Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
const reporter = new UsageTelemetryReporter();
|
|
1499
|
+
await reporter.flush();
|
|
1500
|
+
|
|
1501
|
+
const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
1502
|
+
(c) => c[0] === "telemetry:tool_executed:last_reported_at",
|
|
1503
|
+
);
|
|
1504
|
+
expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
|
|
1505
|
+
expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
|
|
1506
|
+
String(1700000002000),
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
1510
|
+
(c) => c[0] === "telemetry:tool_executed:last_reported_id",
|
|
1511
|
+
);
|
|
1512
|
+
expect(idCalls.length).toBeGreaterThanOrEqual(1);
|
|
1513
|
+
expect(idCalls[idCalls.length - 1][1]).toBe("ti-w2");
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
test("tool_executed resumes from a stored watermark — reported rows are not re-shipped", async () => {
|
|
1517
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1518
|
+
seedToolInvocation({ id: "ti-r1", createdAt: 1700000001000 });
|
|
1519
|
+
seedToolInvocation({ id: "ti-r2", createdAt: 1700000002000 });
|
|
1520
|
+
// A previous flush already reported ti-r1.
|
|
1521
|
+
mockGetMemoryCheckpoint.mockImplementation((key) => {
|
|
1522
|
+
if (key === "telemetry:tool_executed:last_reported_at") {
|
|
1523
|
+
return String(1700000001000);
|
|
1524
|
+
}
|
|
1525
|
+
if (key === "telemetry:tool_executed:last_reported_id") return "ti-r1";
|
|
1526
|
+
return null;
|
|
1527
|
+
});
|
|
1528
|
+
mockFetch.mockImplementation(() =>
|
|
1529
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1532
|
+
const reporter = new UsageTelemetryReporter();
|
|
1533
|
+
await reporter.flush();
|
|
1534
|
+
|
|
1535
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1536
|
+
const body = JSON.parse(
|
|
1537
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1538
|
+
);
|
|
1539
|
+
expect(
|
|
1540
|
+
body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
|
|
1541
|
+
).toEqual(["ti-r2"]);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
test("rows recorded after construction but before the first flush are shipped", async () => {
|
|
1545
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1546
|
+
// Regression coverage for the review finding: the reporter delays its
|
|
1547
|
+
// first flush by 30s+, so a tool used right after daemon startup is
|
|
1548
|
+
// recorded before any flush has run. The construction-time watermark
|
|
1549
|
+
// init must not drop it — only rows from before construction (rows
|
|
1550
|
+
// predating the reporter) stay behind the watermark.
|
|
1551
|
+
const checkpoints = useStatefulCheckpoints();
|
|
1552
|
+
|
|
1553
|
+
const reporter = new UsageTelemetryReporter();
|
|
1554
|
+
const rowCreatedAt =
|
|
1555
|
+
Number(checkpoints.get("telemetry:tool_executed:last_reported_at")) +
|
|
1556
|
+
1000;
|
|
1557
|
+
seedToolInvocation({ id: "ti-pre-first-flush", createdAt: rowCreatedAt });
|
|
1558
|
+
|
|
1559
|
+
await reporter.flush();
|
|
1560
|
+
|
|
1561
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1562
|
+
const body = JSON.parse(
|
|
1563
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1564
|
+
);
|
|
1565
|
+
expect(body.events.length).toBe(1);
|
|
1566
|
+
expect(body.events[0]).toMatchObject({
|
|
1567
|
+
type: "tool_executed",
|
|
1568
|
+
daemon_event_id: "ti-pre-first-flush",
|
|
1569
|
+
recorded_at: rowCreatedAt,
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// The watermark advances to the shipped row, so the next flush resumes
|
|
1573
|
+
// after it instead of re-shipping.
|
|
1574
|
+
expect(checkpoints.get("telemetry:tool_executed:last_reported_at")).toBe(
|
|
1575
|
+
String(rowCreatedAt),
|
|
1576
|
+
);
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
test("absent tool_executed checkpoint is initialized at construction — rows predating the reporter never ship", async () => {
|
|
1580
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1581
|
+
const checkpoints = useStatefulCheckpoints();
|
|
1582
|
+
|
|
1583
|
+
// Rows accumulated before any flush ever advanced the watermark — e.g.
|
|
1584
|
+
// an opt-out period under an older build that gated reporter
|
|
1585
|
+
// construction on collectUsageData while the always-on audit listener
|
|
1586
|
+
// kept writing.
|
|
1587
|
+
seedToolInvocation({
|
|
1588
|
+
id: "ti-opt-out-window",
|
|
1589
|
+
createdAt: Date.now() - 60_000,
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
const reporter = new UsageTelemetryReporter();
|
|
1593
|
+
|
|
1594
|
+
// The checkpoint is persisted immediately at construction so a crash
|
|
1595
|
+
// before the first flush can't re-initialize later.
|
|
1596
|
+
const initialized = checkpoints.get(
|
|
1597
|
+
"telemetry:tool_executed:last_reported_at",
|
|
1598
|
+
);
|
|
1599
|
+
expect(initialized).toBeDefined();
|
|
1600
|
+
|
|
1601
|
+
// A tool that runs after construction ships normally.
|
|
1602
|
+
seedToolInvocation({
|
|
1603
|
+
id: "ti-post-construction",
|
|
1604
|
+
createdAt: Number(initialized) + 1000,
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
await reporter.flush();
|
|
1608
|
+
|
|
1609
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1610
|
+
const body = JSON.parse(
|
|
1611
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1612
|
+
);
|
|
1613
|
+
expect(
|
|
1614
|
+
body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
|
|
1615
|
+
).toEqual(["ti-post-construction"]);
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
test("existing tool_executed checkpoint is respected — construction does not re-initialize", async () => {
|
|
1619
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1620
|
+
const checkpoints = useStatefulCheckpoints({
|
|
1621
|
+
"telemetry:tool_executed:last_reported_at": String(1700000001000),
|
|
1622
|
+
"telemetry:tool_executed:last_reported_id": "ti-already-reported",
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
seedToolInvocation({
|
|
1626
|
+
id: "ti-already-reported",
|
|
1627
|
+
createdAt: 1700000001000,
|
|
1628
|
+
});
|
|
1629
|
+
// Legitimate backlog past the stored watermark — a re-initialization to
|
|
1630
|
+
// Date.now() at construction would silently drop it. Rows past the
|
|
1631
|
+
// watermark are always legitimately shippable: opted-out sessions keep
|
|
1632
|
+
// the watermark advancing via the opt-out flush branch (the reporter
|
|
1633
|
+
// runs even when collection is disabled), so the backlog can only hold
|
|
1634
|
+
// opted-in rows.
|
|
1635
|
+
seedToolInvocation({ id: "ti-backlog", createdAt: 1700000002000 });
|
|
1636
|
+
|
|
1637
|
+
const reporter = new UsageTelemetryReporter();
|
|
1638
|
+
expect(checkpoints.get("telemetry:tool_executed:last_reported_at")).toBe(
|
|
1639
|
+
String(1700000001000),
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
await reporter.flush();
|
|
1643
|
+
|
|
1644
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1645
|
+
const body = JSON.parse(
|
|
1646
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1647
|
+
);
|
|
1648
|
+
expect(
|
|
1649
|
+
body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
|
|
1650
|
+
).toEqual(["ti-backlog"]);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
test("opt-out window never ships across restarts — opted-out flushes keep the watermark ahead of audit rows", async () => {
|
|
1654
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1655
|
+
// A previous opted-in session shipped through this watermark.
|
|
1656
|
+
const checkpoints = useStatefulCheckpoints({
|
|
1657
|
+
"telemetry:tool_executed:last_reported_at": String(1700000001000),
|
|
1658
|
+
"telemetry:tool_executed:last_reported_id": "ti-shipped-opted-in",
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
// Session 1: the user opted out and restarted. The daemon still
|
|
1662
|
+
// constructs and runs the reporter; the always-on audit listener keeps
|
|
1663
|
+
// writing rows. Every opted-out flush (5-minute cycle plus the final
|
|
1664
|
+
// flush in stop()) advances the watermark past them without sending.
|
|
1665
|
+
mockCollectUsageData = false;
|
|
1666
|
+
const optOutRowCreatedAt = Date.now() - 5_000;
|
|
1667
|
+
seedToolInvocation({
|
|
1668
|
+
id: "ti-opt-out-window",
|
|
1669
|
+
createdAt: optOutRowCreatedAt,
|
|
1670
|
+
});
|
|
1671
|
+
const optedOutReporter = new UsageTelemetryReporter();
|
|
1672
|
+
await optedOutReporter.stop(); // shutdown path: runs the final flush
|
|
1673
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
1674
|
+
const advanced = Number(
|
|
1675
|
+
checkpoints.get("telemetry:tool_executed:last_reported_at"),
|
|
1676
|
+
);
|
|
1677
|
+
expect(advanced).toBeGreaterThan(optOutRowCreatedAt);
|
|
1678
|
+
|
|
1679
|
+
// Session 2: the user opts back in and restarts. Only rows recorded
|
|
1680
|
+
// after the opt-out epoch ship — the opt-out-window row never does.
|
|
1681
|
+
mockCollectUsageData = true;
|
|
1682
|
+
seedToolInvocation({ id: "ti-after-opt-in", createdAt: advanced + 1000 });
|
|
1683
|
+
const reporter = new UsageTelemetryReporter();
|
|
1684
|
+
await reporter.flush();
|
|
1685
|
+
|
|
1686
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1687
|
+
const body = JSON.parse(
|
|
1688
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1689
|
+
);
|
|
1690
|
+
expect(
|
|
1691
|
+
body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
|
|
1692
|
+
).toEqual(["ti-after-opt-in"]);
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
test("opt-out row written in the same millisecond as the opt-out flush never ships after re-opt-in", async () => {
|
|
1696
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1697
|
+
// A previous opted-in session left a low-sorting ID watermark behind.
|
|
1698
|
+
const checkpoints = useStatefulCheckpoints({
|
|
1699
|
+
"telemetry:tool_executed:last_reported_at": String(1700000001000),
|
|
1700
|
+
"telemetry:tool_executed:last_reported_id":
|
|
1701
|
+
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
// Opted-out flush: advances the timestamp watermark to Date.now() and
|
|
1705
|
+
// must also pin the ID watermark to the high-sorting sentinel.
|
|
1706
|
+
mockCollectUsageData = false;
|
|
1707
|
+
const optedOutReporter = new UsageTelemetryReporter();
|
|
1708
|
+
await optedOutReporter.flush();
|
|
1709
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
1710
|
+
const watermark = Number(
|
|
1711
|
+
checkpoints.get("telemetry:tool_executed:last_reported_at"),
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
// An audit row written in the SAME millisecond as the opt-out flush's
|
|
1715
|
+
// Date.now(), with a UUID sorting above the stale pre-opt-out ID
|
|
1716
|
+
// watermark. With only the timestamp watermark advanced, the compound
|
|
1717
|
+
// cursor's `createdAt == watermark AND id > afterId` arm would match it.
|
|
1718
|
+
seedToolInvocation({
|
|
1719
|
+
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
1720
|
+
createdAt: watermark,
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
// Re-opt-in: only rows strictly after the opt-out epoch ship.
|
|
1724
|
+
mockCollectUsageData = true;
|
|
1725
|
+
seedToolInvocation({ id: "ti-after-opt-in", createdAt: watermark + 1000 });
|
|
1726
|
+
const reporter = new UsageTelemetryReporter();
|
|
1727
|
+
await reporter.flush();
|
|
1728
|
+
|
|
1729
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1730
|
+
const body = JSON.parse(
|
|
1731
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1732
|
+
);
|
|
1733
|
+
expect(
|
|
1734
|
+
body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
|
|
1735
|
+
).toEqual(["ti-after-opt-in"]);
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
test("checkpoint store failure during construction is non-fatal — degraded-DB daemons still start", () => {
|
|
1739
|
+
// initializeDb() failures are tolerated at daemon startup (degraded
|
|
1740
|
+
// mode), so the constructor's checkpoint init must never throw out of
|
|
1741
|
+
// the constructor and abort the daemon.
|
|
1742
|
+
mockGetMemoryCheckpoint.mockImplementation(() => {
|
|
1743
|
+
throw new Error("database disk image is malformed");
|
|
1744
|
+
});
|
|
1745
|
+
expect(() => new UsageTelemetryReporter()).not.toThrow();
|
|
1746
|
+
|
|
1747
|
+
// The write path failing is equally non-fatal.
|
|
1748
|
+
mockGetMemoryCheckpoint.mockImplementation(() => null);
|
|
1749
|
+
mockSetMemoryCheckpoint.mockImplementation(() => {
|
|
1750
|
+
throw new Error("database disk image is malformed");
|
|
1751
|
+
});
|
|
1752
|
+
expect(() => new UsageTelemetryReporter()).not.toThrow();
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// -------------------------------------------------------------------------
|
|
1756
|
+
// Skill-loaded events
|
|
1757
|
+
// -------------------------------------------------------------------------
|
|
1758
|
+
|
|
1759
|
+
test("skill_loaded events ship metadata with attribution and null passthrough", async () => {
|
|
1760
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1761
|
+
recordSkillLoadedEvent({
|
|
1762
|
+
conversationId: "conv-skill",
|
|
1763
|
+
skillName: "web-research",
|
|
1764
|
+
skillUpdatedAt: "2026-06-01T00:00:00.000Z",
|
|
1765
|
+
provider: "anthropic",
|
|
1766
|
+
model: "model-a",
|
|
1767
|
+
inferenceProfile: "balanced",
|
|
1768
|
+
inferenceProfileSource: "active",
|
|
1769
|
+
});
|
|
1770
|
+
// Minimal record — optional fields persist as null and ship as null.
|
|
1771
|
+
recordSkillLoadedEvent({ skillName: "tasks" });
|
|
1772
|
+
mockFetch.mockImplementation(() =>
|
|
1773
|
+
Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
|
|
1774
|
+
);
|
|
1775
|
+
|
|
1776
|
+
const reporter = new UsageTelemetryReporter();
|
|
1777
|
+
await reporter.flush();
|
|
1778
|
+
|
|
1779
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1780
|
+
const body = JSON.parse(
|
|
1781
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1782
|
+
);
|
|
1783
|
+
expect(body.events.length).toBe(2);
|
|
1784
|
+
|
|
1785
|
+
const byName: Record<string, Record<string, unknown>> = {};
|
|
1786
|
+
for (const e of body.events as Array<{ skill_name: string }>) {
|
|
1787
|
+
byName[e.skill_name] = e;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
expect(byName["web-research"]).toMatchObject({
|
|
1791
|
+
type: "skill_loaded",
|
|
1792
|
+
skill_name: "web-research",
|
|
1793
|
+
skill_updated_at: "2026-06-01T00:00:00.000Z",
|
|
1794
|
+
conversation_id: "conv-skill",
|
|
1795
|
+
provider: "anthropic",
|
|
1796
|
+
model: "model-a",
|
|
1797
|
+
inference_profile: "balanced",
|
|
1798
|
+
inference_profile_source: "active",
|
|
1799
|
+
assistant_version: "1.2.3-test",
|
|
1800
|
+
});
|
|
1801
|
+
expect(typeof byName["web-research"].daemon_event_id).toBe("string");
|
|
1802
|
+
expect(typeof byName["web-research"].recorded_at).toBe("number");
|
|
1803
|
+
expect(byName["tasks"]).toMatchObject({
|
|
1804
|
+
type: "skill_loaded",
|
|
1805
|
+
skill_name: "tasks",
|
|
1806
|
+
skill_updated_at: null,
|
|
1807
|
+
conversation_id: null,
|
|
1808
|
+
provider: null,
|
|
1809
|
+
model: null,
|
|
1810
|
+
inference_profile: null,
|
|
1811
|
+
inference_profile_source: null,
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
test("skill_loaded watermark advances to the last reported row on success", async () => {
|
|
1816
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1817
|
+
recordSkillLoadedEvent({ skillName: "web-research" });
|
|
1818
|
+
recordSkillLoadedEvent({ skillName: "tasks" });
|
|
1819
|
+
mockFetch.mockImplementation(() =>
|
|
1820
|
+
Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
|
|
1821
|
+
);
|
|
1822
|
+
|
|
1823
|
+
// The last row by the reporter's (createdAt, id) cursor order is the one
|
|
1824
|
+
// whose watermark should be persisted after a successful upload.
|
|
1825
|
+
const rows = getDb()
|
|
1826
|
+
.select()
|
|
1827
|
+
.from(skillLoadedEvents)
|
|
1828
|
+
.orderBy(skillLoadedEvents.createdAt, skillLoadedEvents.id)
|
|
1829
|
+
.all();
|
|
1830
|
+
const lastRow = rows[rows.length - 1];
|
|
1831
|
+
|
|
1832
|
+
const reporter = new UsageTelemetryReporter();
|
|
1833
|
+
await reporter.flush();
|
|
1834
|
+
|
|
1835
|
+
const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
1836
|
+
(c) => c[0] === "telemetry:skill_loaded:last_reported_at",
|
|
1837
|
+
);
|
|
1838
|
+
expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
|
|
1839
|
+
expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
|
|
1840
|
+
String(lastRow.createdAt),
|
|
1841
|
+
);
|
|
1842
|
+
|
|
1843
|
+
const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
1844
|
+
(c) => c[0] === "telemetry:skill_loaded:last_reported_id",
|
|
1845
|
+
);
|
|
1846
|
+
expect(idCalls.length).toBeGreaterThanOrEqual(1);
|
|
1847
|
+
expect(idCalls[idCalls.length - 1][1]).toBe(lastRow.id);
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
test("batch recursion when skill_loaded returns a full batch, capped at 10", async () => {
|
|
1851
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1852
|
+
// Exactly BATCH_SIZE rows; the mocked checkpoints never persist, so each
|
|
1853
|
+
// recursion re-reads the same full batch until the cap.
|
|
1854
|
+
const fullBatch = Array.from({ length: 500 }, (_, i) => ({
|
|
1855
|
+
id: `sle-batch-${String(i).padStart(3, "0")}`,
|
|
1856
|
+
createdAt: 1700000000000 + i,
|
|
1857
|
+
skillName: "web-research",
|
|
1858
|
+
}));
|
|
1859
|
+
getDb().insert(skillLoadedEvents).values(fullBatch).run();
|
|
1860
|
+
mockFetch.mockImplementation(() =>
|
|
1861
|
+
Promise.resolve(new Response('{"accepted":500}', { status: 200 })),
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
const reporter = new UsageTelemetryReporter();
|
|
1865
|
+
await reporter.flush();
|
|
1866
|
+
|
|
1867
|
+
// MAX_CONSECUTIVE_BATCHES = 10
|
|
1868
|
+
expect(mockFetch).toHaveBeenCalledTimes(10);
|
|
1869
|
+
});
|
|
1341
1870
|
});
|