@vellumai/assistant 0.8.12 → 0.9.0-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +0 -14
- package/ARCHITECTURE.md +45 -45
- package/README.md +1 -1
- package/bun.lock +200 -154
- package/docs/architecture/integrations.md +3 -3
- package/docs/architecture/memory.md +2 -2
- package/docs/architecture/security.md +10 -10
- package/docs/runbook-trusted-contacts.md +12 -12
- package/docs/skills.md +6 -6
- package/docs/workflows-testing.md +221 -0
- package/docs/workflows.md +510 -0
- package/examples/plugins/echo/README.md +5 -5
- package/knip.json +2 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
- package/openapi.yaml +7197 -5708
- package/package.json +8 -4
- package/scripts/generate-openapi.ts +66 -114
- package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
- package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
- package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
- package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
- package/src/__tests__/app-compiler.test.ts +7 -1
- package/src/__tests__/app-dir-path-guard.test.ts +27 -3
- package/src/__tests__/app-executors.test.ts +43 -0
- package/src/__tests__/approval-cascade.test.ts +0 -5
- package/src/__tests__/approval-routes-http.test.ts +91 -0
- package/src/__tests__/assistant-stream-state.test.ts +107 -0
- package/src/__tests__/browser-fill-credential.test.ts +3 -3
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +63 -7
- package/src/__tests__/compaction-trail-store.test.ts +74 -1
- package/src/__tests__/compaction.benchmark.test.ts +63 -41
- package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
- package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
- package/src/__tests__/conversation-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
- package/src/__tests__/conversation-speed-override.test.ts +0 -5
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
- package/src/__tests__/credential-broker-server-use.test.ts +2 -2
- package/src/__tests__/credential-broker.test.ts +1 -1
- package/src/__tests__/credential-prompt-route.test.ts +417 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault.test.ts +37 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
- package/src/__tests__/dynamic-page-surface.test.ts +219 -0
- package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
- package/src/__tests__/gateway-flag-listener.test.ts +24 -7
- package/src/__tests__/guardian-action-sweep.test.ts +56 -219
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
- package/src/__tests__/llm-resolver.test.ts +115 -0
- package/src/__tests__/managed-profile-guard.test.ts +6 -5
- package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
- package/src/__tests__/media-generate-image.test.ts +20 -9
- package/src/__tests__/mock-gateway-ipc.ts +23 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/normalize-onboarding.test.ts +26 -0
- package/src/__tests__/notification-decision-strategy.test.ts +4 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
- package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
- package/src/__tests__/require-fresh-approval.test.ts +425 -1
- package/src/__tests__/resolve-app-id.test.ts +56 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
- package/src/__tests__/schedule-routes.test.ts +257 -4
- package/src/__tests__/schedule-store.test.ts +60 -0
- package/src/__tests__/schedule-tools.test.ts +247 -2
- package/src/__tests__/skill-execute-input.test.ts +85 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
- package/src/__tests__/task-scheduler.test.ts +299 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
- package/src/__tests__/tool-result-spool.test.ts +3 -1
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
- package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
- package/src/agent/compaction-circuit.ts +11 -0
- package/src/agent/loop.ts +181 -12
- package/src/api/constants/call-sites.ts +12 -0
- package/src/api/events/assistant-thinking-delta.ts +10 -0
- package/src/api/events/usage-update.ts +7 -0
- package/src/api/index.ts +4 -1
- package/src/api/responses/memory-v3-selection-log.ts +18 -11
- package/src/approvals/approval-primitive.ts +2 -2
- package/src/background-wake/background-wake-routes.test.ts +5 -2
- package/src/bundler/compiler-tools.ts +1 -1
- package/src/bundler/package-resolver.ts +0 -1
- package/src/calls/call-domain.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +16 -93
- package/src/cli/AGENTS.md +4 -0
- package/src/cli/commands/__tests__/schedules.test.ts +430 -1
- package/src/cli/commands/credentials.ts +28 -24
- package/src/cli/commands/image-generation.ts +23 -9
- package/src/cli/commands/notifications.ts +1 -1
- package/src/cli/commands/plugins.ts +89 -46
- package/src/cli/commands/schedules.ts +384 -11
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
- package/src/cli/lib/inspect-plugin.ts +62 -1
- package/src/cli/lib/install-from-github.ts +52 -5
- package/src/cli/lib/upgrade-plugin.ts +18 -0
- package/src/config/__tests__/workflows-schema.test.ts +60 -0
- package/src/config/bundled-skills/acp/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
- package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
- package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
- package/src/config/bundled-skills/schedule/SKILL.md +7 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
- package/src/config/bundled-skills/workflows/SKILL.md +214 -0
- package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
- package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
- package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
- package/src/config/bundled-tool-registry.ts +14 -2
- package/src/config/call-site-defaults.ts +5 -0
- package/src/config/feature-flag-registry.json +12 -4
- package/src/config/llm-context-resolution.ts +8 -0
- package/src/config/llm-resolver.ts +30 -0
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
- package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
- package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
- package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/heartbeat.ts +4 -1
- package/src/config/schemas/llm.ts +33 -26
- package/src/config/schemas/memory-retrospective.ts +19 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/services.ts +5 -2
- package/src/config/schemas/workflows.ts +42 -0
- package/src/config/skills.ts +3 -3
- package/src/context/compactor.ts +273 -39
- package/src/context/post-turn-tool-result-truncation.ts +23 -6
- package/src/context/tool-result-spool.ts +12 -17
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/process-manager.ts +37 -3
- package/src/credential-execution/prompted-credential.ts +205 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
- package/src/daemon/conversation-process.ts +11 -2
- package/src/daemon/conversation-surfaces.ts +167 -3
- package/src/daemon/conversation-tool-setup.ts +103 -26
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +115 -11
- package/src/daemon/handlers/shared.ts +26 -14
- package/src/daemon/host-cu-proxy.ts +15 -12
- package/src/daemon/host-file-proxy.ts +15 -12
- package/src/daemon/host-transfer-proxy.ts +30 -24
- package/src/daemon/lifecycle.ts +40 -3
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/messages.ts +2 -10
- package/src/daemon/message-types/workflows.ts +49 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
- package/src/daemon/process-message.ts +6 -0
- package/src/daemon/tool-setup-types.ts +57 -0
- package/src/daemon/wake-conversation-ops.ts +18 -0
- package/src/heartbeat/heartbeat-run-store.ts +8 -2
- package/src/home/feed-types.ts +1 -1
- package/src/ipc/gateway-flag-listener.ts +28 -6
- package/src/mcp/mcp-auth-state.ts +8 -20
- package/src/media/__tests__/image-models.test.ts +57 -0
- package/src/media/image-models.ts +66 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
- package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
- package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
- package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
- package/src/memory/auto-analysis-enqueue.ts +14 -1
- package/src/memory/compaction-log-store-clickhouse.ts +6 -4
- package/src/memory/conversation-crud.ts +9 -2
- package/src/memory/conversation-disk-view.ts +1 -1
- package/src/memory/conversation-queries.ts +22 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/db-maintenance.ts +16 -0
- package/src/memory/embedding-runtime-manager.ts +1 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
- package/src/memory/llm-request-log-source-local.ts +19 -1
- package/src/memory/llm-request-log-source.ts +35 -6
- package/src/memory/llm-request-log-store.ts +90 -2
- package/src/memory/memory-retrospective-constants.ts +9 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -6
- package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
- package/src/memory/memory-retrospective-job.ts +500 -208
- package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
- package/src/memory/memory-retrospective-state.ts +85 -2
- package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
- package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
- package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
- package/src/memory/migrations/284-workflow-runs.ts +51 -0
- package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
- package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
- package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
- package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
- package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
- package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
- package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/preloaded-apps.ts +116 -0
- package/src/memory/schema/infrastructure.ts +4 -0
- package/src/memory/schema/memory-core.ts +4 -0
- package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
- package/src/memory/v2/__tests__/page-store.test.ts +13 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -6
- package/src/memory/v2/page-store.ts +4 -3
- package/src/memory/v2/qdrant.ts +42 -3
- package/src/memory/v2/types.ts +16 -10
- package/src/messaging/draft-store.ts +1 -1
- package/src/notifications/access-request-copy.ts +200 -113
- package/src/notifications/adapters/slack.ts +250 -111
- package/src/notifications/adapters/telegram.ts +7 -44
- package/src/notifications/approval-card-builder.ts +93 -0
- package/src/notifications/broadcaster.ts +74 -0
- package/src/notifications/conversation-pairing.ts +8 -6
- package/src/notifications/copy-composer.ts +32 -26
- package/src/notifications/decision-engine.ts +59 -7
- package/src/notifications/guardian-question-mode.ts +145 -155
- package/src/notifications/home-feed-side-effect.ts +28 -11
- package/src/notifications/notification-utils.ts +66 -0
- package/src/notifications/signal.ts +6 -0
- package/src/notifications/tool-approval-copy.ts +142 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/threshold.ts +11 -0
- package/src/plugin-api/types.ts +16 -4
- package/src/plugins/defaults/compaction/window-manager.ts +44 -0
- package/src/plugins/defaults/index.ts +46 -0
- package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
- package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
- package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
- package/src/prompts/__tests__/system-prompt.test.ts +100 -1
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
- package/src/prompts/normalize-onboarding.ts +2 -0
- package/src/prompts/persona-resolver.ts +3 -0
- package/src/prompts/system-prompt.ts +51 -2
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
- package/src/prompts/templates/system-sections.ts +8 -3
- package/src/providers/call-site-routing.ts +6 -3
- package/src/providers/fireworks/client.ts +3 -0
- package/src/providers/inference/auth.ts +52 -46
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +47 -9
- package/src/providers/openai/coerce-object-args.ts +104 -0
- package/src/providers/retry.ts +8 -5
- package/src/providers/types.ts +10 -0
- package/src/runtime/__tests__/agent-wake.test.ts +629 -7
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake.ts +302 -51
- package/src/runtime/assistant-stream-state.ts +141 -8
- package/src/runtime/background-job-runner.ts +9 -0
- package/src/runtime/channel-approval-types.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +6 -5
- package/src/runtime/channel-invite-transports/voice.ts +2 -2
- package/src/runtime/channel-invite-types.ts +4 -2
- package/src/runtime/channel-retry-sweep.ts +19 -41
- package/src/runtime/finalize-event-delivery.ts +72 -0
- package/src/runtime/guardian-action-message-composer.ts +0 -54
- package/src/runtime/http-server.ts +6 -14
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/message-composer-types.ts +0 -9
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
- package/src/runtime/middleware/auth.ts +27 -3
- package/src/runtime/middleware/rate-limiter.ts +28 -1
- package/src/runtime/migrations/vbundle-builder.ts +6 -5
- package/src/runtime/pending-interactions.ts +20 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
- package/src/runtime/routes/approval-routes.ts +35 -8
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
- package/src/runtime/routes/btw-routes.ts +37 -1
- package/src/runtime/routes/channel-delivery-routes.ts +11 -7
- package/src/runtime/routes/channel-route-definitions.ts +3 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -1
- package/src/runtime/routes/consolidation-routes.ts +17 -13
- package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
- package/src/runtime/routes/conversation-list-routes.ts +41 -4
- package/src/runtime/routes/conversation-query-routes.ts +196 -11
- package/src/runtime/routes/conversation-routes.ts +15 -1
- package/src/runtime/routes/credential-prompt-routes.ts +39 -17
- package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
- package/src/runtime/routes/heartbeat-routes.ts +18 -13
- package/src/runtime/routes/image-generation-routes.ts +20 -2
- package/src/runtime/routes/inbound-message-handler.ts +13 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/log-export/AGENTS.md +1 -1
- package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
- package/src/runtime/routes/migration-routes.ts +5 -9
- package/src/runtime/routes/plugins-routes.ts +26 -0
- package/src/runtime/routes/ps-routes.ts +10 -8
- package/src/runtime/routes/retrospective-routes.ts +235 -0
- package/src/runtime/routes/runs-pagination.ts +75 -0
- package/src/runtime/routes/schedule-routes.ts +367 -59
- package/src/runtime/routes/sounds-config-routes.ts +239 -0
- package/src/runtime/routes/surface-action-routes.ts +84 -4
- package/src/runtime/routes/workflow-routes.test.ts +372 -0
- package/src/runtime/routes/workflow-routes.ts +363 -0
- package/src/runtime/routes/workspace-routes.test.ts +61 -1
- package/src/runtime/routes/workspace-routes.ts +26 -1
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
- package/src/runtime/services/analyze-conversation.ts +26 -13
- package/src/schedule/inference-profile.ts +28 -0
- package/src/schedule/schedule-store.ts +152 -4
- package/src/schedule/scheduler-types.ts +6 -0
- package/src/schedule/scheduler.ts +96 -0
- package/src/security/secret-allowlist.ts +1 -1
- package/src/skills/path-classifier.ts +1 -1
- package/src/tools/apps/executors.ts +23 -0
- package/src/tools/apps/resolve-app-id.ts +42 -0
- package/src/tools/browser/browser-execution.ts +9 -11
- package/src/tools/credentials/broker.ts +4 -4
- package/src/tools/credentials/vault.ts +26 -137
- package/src/tools/executor.ts +69 -0
- package/src/tools/flag-gated-tools.test.ts +76 -0
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/registry.ts +51 -0
- package/src/tools/schedule/create.ts +77 -1
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +73 -1
- package/src/tools/skills/execute.ts +56 -0
- package/src/tools/terminal/shell.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +91 -2
- package/src/tools/workflows/manage-workflows.ts +183 -0
- package/src/tools/workflows/run-workflow.test.ts +442 -0
- package/src/tools/workflows/run-workflow.ts +88 -0
- package/src/types/onboarding-context.ts +2 -0
- package/src/usage/attribution.ts +24 -0
- package/src/util/canonicalize-identity.ts +12 -3
- package/src/util/platform.ts +17 -17
- package/src/watcher/__tests__/engine.test.ts +24 -0
- package/src/watcher/__tests__/telemetry.test.ts +135 -0
- package/src/watcher/engine.ts +7 -0
- package/src/watcher/telemetry.ts +74 -0
- package/src/workflows/capabilities.test.ts +365 -0
- package/src/workflows/capabilities.ts +359 -0
- package/src/workflows/deterministic-stringify.ts +27 -0
- package/src/workflows/engine-integration.test.ts +656 -0
- package/src/workflows/engine.test.ts +1144 -0
- package/src/workflows/engine.ts +1078 -0
- package/src/workflows/fanout-load.test.ts +168 -0
- package/src/workflows/journal-store.test.ts +369 -0
- package/src/workflows/journal-store.ts +470 -0
- package/src/workflows/leaf-runner.test.ts +704 -0
- package/src/workflows/leaf-runner.ts +589 -0
- package/src/workflows/library.test.ts +134 -0
- package/src/workflows/library.ts +124 -0
- package/src/workflows/run-manager.test.ts +711 -0
- package/src/workflows/run-manager.ts +593 -0
- package/src/workflows/sandbox-escape.test.ts +339 -0
- package/src/workflows/sandbox.test.ts +251 -0
- package/src/workflows/sandbox.ts +447 -0
- package/src/workspace/adaptive-thinking-repair.ts +33 -11
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
- package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
- package/src/workspace/migrations/031-drop-user-md.ts +1 -4
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
- package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
- package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
- package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
- package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
- package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/migrations/runner.ts +1 -1
- package/tsconfig.json +1 -1
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
- package/src/__tests__/guardian-action-store.test.ts +0 -106
- package/src/daemon/guardian-action-generators.ts +0 -71
- package/src/memory/guardian-action-store.ts +0 -484
- package/src/runtime/guardian-action-grant-minter.ts +0 -150
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import { getSqliteFrom } from "../../db-connection.js";
|
|
7
|
+
import * as schema from "../../schema.js";
|
|
8
|
+
import { migrateContactChannelsUniqueExtUser } from "../289-contact-channels-unique-ext-user.js";
|
|
9
|
+
|
|
10
|
+
interface IndexRow {
|
|
11
|
+
name: string;
|
|
12
|
+
unique: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ChannelRow {
|
|
16
|
+
id: string;
|
|
17
|
+
contact_id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
address: string;
|
|
20
|
+
external_user_id: string | null;
|
|
21
|
+
status: string;
|
|
22
|
+
updated_at: number | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createTestDb() {
|
|
26
|
+
const sqlite = new Database(":memory:");
|
|
27
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
28
|
+
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
29
|
+
return drizzle(sqlite, { schema });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bootstrap minimal contact_channels + contacts tables with a case-sensitive
|
|
34
|
+
* unique index on (type, address) and a non-unique index on
|
|
35
|
+
* (type, external_user_id) — the pre-migration state.
|
|
36
|
+
*/
|
|
37
|
+
function bootstrap(db: ReturnType<typeof createTestDb>): void {
|
|
38
|
+
const raw = getSqliteFrom(db);
|
|
39
|
+
raw.exec(/*sql*/ `
|
|
40
|
+
CREATE TABLE contacts (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
display_name TEXT NOT NULL,
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
updated_at INTEGER NOT NULL,
|
|
45
|
+
role TEXT NOT NULL DEFAULT 'contact',
|
|
46
|
+
contact_type TEXT NOT NULL DEFAULT 'human'
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
raw.exec(/*sql*/ `
|
|
50
|
+
CREATE TABLE contact_channels (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
53
|
+
type TEXT NOT NULL,
|
|
54
|
+
address TEXT NOT NULL,
|
|
55
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
external_user_id TEXT,
|
|
57
|
+
external_chat_id TEXT,
|
|
58
|
+
status TEXT NOT NULL DEFAULT 'unverified',
|
|
59
|
+
policy TEXT NOT NULL DEFAULT 'allow',
|
|
60
|
+
interaction_count INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
updated_at INTEGER,
|
|
62
|
+
created_at INTEGER NOT NULL
|
|
63
|
+
)
|
|
64
|
+
`);
|
|
65
|
+
raw.exec(/*sql*/ `
|
|
66
|
+
CREATE UNIQUE INDEX idx_contact_channels_type_address
|
|
67
|
+
ON contact_channels(type, address)
|
|
68
|
+
`);
|
|
69
|
+
raw.exec(/*sql*/ `
|
|
70
|
+
CREATE INDEX idx_contact_channels_type_ext_user
|
|
71
|
+
ON contact_channels(type, external_user_id)
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function insertContact(raw: Database, id: string): void {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
raw
|
|
78
|
+
.query(
|
|
79
|
+
`INSERT INTO contacts (id, display_name, created_at, updated_at) VALUES (?, ?, ?, ?)`,
|
|
80
|
+
)
|
|
81
|
+
.run(id, `Contact ${id}`, now, now);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function insertChannel(
|
|
85
|
+
raw: Database,
|
|
86
|
+
opts: {
|
|
87
|
+
id: string;
|
|
88
|
+
contactId: string;
|
|
89
|
+
type: string;
|
|
90
|
+
address: string;
|
|
91
|
+
externalUserId: string | null;
|
|
92
|
+
status: string;
|
|
93
|
+
updatedAt: number;
|
|
94
|
+
},
|
|
95
|
+
): void {
|
|
96
|
+
raw
|
|
97
|
+
.query(
|
|
98
|
+
`INSERT INTO contact_channels (id, contact_id, type, address, external_user_id, status, updated_at, created_at)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
100
|
+
)
|
|
101
|
+
.run(
|
|
102
|
+
opts.id,
|
|
103
|
+
opts.contactId,
|
|
104
|
+
opts.type,
|
|
105
|
+
opts.address,
|
|
106
|
+
opts.externalUserId,
|
|
107
|
+
opts.status,
|
|
108
|
+
opts.updatedAt,
|
|
109
|
+
opts.updatedAt,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getAllChannels(raw: Database): ChannelRow[] {
|
|
114
|
+
return raw
|
|
115
|
+
.query<ChannelRow, []>(`SELECT * FROM contact_channels ORDER BY id`)
|
|
116
|
+
.all();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getIndexes(raw: Database): IndexRow[] {
|
|
120
|
+
return raw
|
|
121
|
+
.query<
|
|
122
|
+
IndexRow,
|
|
123
|
+
[]
|
|
124
|
+
>(`SELECT name, "unique" FROM pragma_index_list('contact_channels')`)
|
|
125
|
+
.all();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe("migration 287 — dedup case collisions + drop ext_user indexes", () => {
|
|
129
|
+
test("no-op on clean database (no duplicates), preserves original casing", () => {
|
|
130
|
+
const db = createTestDb();
|
|
131
|
+
const raw = getSqliteFrom(db);
|
|
132
|
+
bootstrap(db);
|
|
133
|
+
|
|
134
|
+
insertContact(raw, "c1");
|
|
135
|
+
insertChannel(raw, {
|
|
136
|
+
id: "ch1",
|
|
137
|
+
contactId: "c1",
|
|
138
|
+
type: "slack",
|
|
139
|
+
address: "U111",
|
|
140
|
+
externalUserId: "U111",
|
|
141
|
+
status: "active",
|
|
142
|
+
updatedAt: 1000,
|
|
143
|
+
});
|
|
144
|
+
insertChannel(raw, {
|
|
145
|
+
id: "ch2",
|
|
146
|
+
contactId: "c1",
|
|
147
|
+
type: "telegram",
|
|
148
|
+
address: "T222",
|
|
149
|
+
externalUserId: "T222",
|
|
150
|
+
status: "active",
|
|
151
|
+
updatedAt: 1000,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
155
|
+
|
|
156
|
+
const channels = getAllChannels(raw);
|
|
157
|
+
expect(channels).toHaveLength(2);
|
|
158
|
+
expect(channels.map((c) => c.id).sort()).toEqual(["ch1", "ch2"]);
|
|
159
|
+
// Addresses preserve original casing
|
|
160
|
+
expect(channels.find((c) => c.id === "ch1")!.address).toBe("U111");
|
|
161
|
+
expect(channels.find((c) => c.id === "ch2")!.address).toBe("T222");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("preserves original Slack address casing", () => {
|
|
165
|
+
const db = createTestDb();
|
|
166
|
+
const raw = getSqliteFrom(db);
|
|
167
|
+
bootstrap(db);
|
|
168
|
+
|
|
169
|
+
insertContact(raw, "c1");
|
|
170
|
+
insertChannel(raw, {
|
|
171
|
+
id: "ch1",
|
|
172
|
+
contactId: "c1",
|
|
173
|
+
type: "slack",
|
|
174
|
+
address: "U12345ABC",
|
|
175
|
+
externalUserId: "U12345ABC",
|
|
176
|
+
status: "active",
|
|
177
|
+
updatedAt: 1000,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
181
|
+
|
|
182
|
+
const channels = getAllChannels(raw);
|
|
183
|
+
expect(channels).toHaveLength(1);
|
|
184
|
+
expect(channels[0]!.address).toBe("U12345ABC");
|
|
185
|
+
expect(channels[0]!.external_user_id).toBe("U12345ABC");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("deduplicates historical case collisions (active wins over unverified)", () => {
|
|
189
|
+
const db = createTestDb();
|
|
190
|
+
const raw = getSqliteFrom(db);
|
|
191
|
+
bootstrap(db);
|
|
192
|
+
|
|
193
|
+
insertContact(raw, "c1");
|
|
194
|
+
insertContact(raw, "c2");
|
|
195
|
+
|
|
196
|
+
// Two Slack channels with different casing — historical inconsistency.
|
|
197
|
+
insertChannel(raw, {
|
|
198
|
+
id: "ch-upper",
|
|
199
|
+
contactId: "c1",
|
|
200
|
+
type: "slack",
|
|
201
|
+
address: "U12345",
|
|
202
|
+
externalUserId: "U12345",
|
|
203
|
+
status: "active",
|
|
204
|
+
updatedAt: 1000,
|
|
205
|
+
});
|
|
206
|
+
insertChannel(raw, {
|
|
207
|
+
id: "ch-lower",
|
|
208
|
+
contactId: "c2",
|
|
209
|
+
type: "slack",
|
|
210
|
+
address: "u12345",
|
|
211
|
+
externalUserId: "U12345",
|
|
212
|
+
status: "unverified",
|
|
213
|
+
updatedAt: 900,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
217
|
+
|
|
218
|
+
const channels = getAllChannels(raw);
|
|
219
|
+
// Only one survives — active wins over unverified
|
|
220
|
+
expect(channels).toHaveLength(1);
|
|
221
|
+
expect(channels[0]!.id).toBe("ch-upper");
|
|
222
|
+
expect(channels[0]!.address).toBe("U12345");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("deduplicates rows keeping blocked over active (preserves deny state)", () => {
|
|
226
|
+
const db = createTestDb();
|
|
227
|
+
const raw = getSqliteFrom(db);
|
|
228
|
+
bootstrap(db);
|
|
229
|
+
|
|
230
|
+
insertContact(raw, "c1");
|
|
231
|
+
insertContact(raw, "c2");
|
|
232
|
+
|
|
233
|
+
insertChannel(raw, {
|
|
234
|
+
id: "ch-active",
|
|
235
|
+
contactId: "c1",
|
|
236
|
+
type: "slack",
|
|
237
|
+
address: "U789",
|
|
238
|
+
externalUserId: "U789",
|
|
239
|
+
status: "active",
|
|
240
|
+
updatedAt: 1000,
|
|
241
|
+
});
|
|
242
|
+
insertChannel(raw, {
|
|
243
|
+
id: "ch-blocked",
|
|
244
|
+
contactId: "c2",
|
|
245
|
+
type: "slack",
|
|
246
|
+
address: "u789",
|
|
247
|
+
externalUserId: "U789",
|
|
248
|
+
status: "blocked",
|
|
249
|
+
updatedAt: 800,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
253
|
+
|
|
254
|
+
const channels = getAllChannels(raw);
|
|
255
|
+
expect(channels).toHaveLength(1);
|
|
256
|
+
expect(channels[0]!.id).toBe("ch-blocked");
|
|
257
|
+
expect(channels[0]!.status).toBe("blocked");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("deduplicates rows keeping most recent when same status", () => {
|
|
261
|
+
const db = createTestDb();
|
|
262
|
+
const raw = getSqliteFrom(db);
|
|
263
|
+
bootstrap(db);
|
|
264
|
+
|
|
265
|
+
insertContact(raw, "c1");
|
|
266
|
+
insertContact(raw, "c2");
|
|
267
|
+
|
|
268
|
+
insertChannel(raw, {
|
|
269
|
+
id: "ch-old",
|
|
270
|
+
contactId: "c1",
|
|
271
|
+
type: "slack",
|
|
272
|
+
address: "U456",
|
|
273
|
+
externalUserId: "U456",
|
|
274
|
+
status: "unverified",
|
|
275
|
+
updatedAt: 500,
|
|
276
|
+
});
|
|
277
|
+
insertChannel(raw, {
|
|
278
|
+
id: "ch-new",
|
|
279
|
+
contactId: "c2",
|
|
280
|
+
type: "slack",
|
|
281
|
+
address: "u456",
|
|
282
|
+
externalUserId: "U456",
|
|
283
|
+
status: "unverified",
|
|
284
|
+
updatedAt: 1000,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
288
|
+
|
|
289
|
+
const channels = getAllChannels(raw);
|
|
290
|
+
expect(channels).toHaveLength(1);
|
|
291
|
+
expect(channels[0]!.id).toBe("ch-new");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("does not touch rows with different addresses on different types", () => {
|
|
295
|
+
const db = createTestDb();
|
|
296
|
+
const raw = getSqliteFrom(db);
|
|
297
|
+
bootstrap(db);
|
|
298
|
+
|
|
299
|
+
insertContact(raw, "c1");
|
|
300
|
+
|
|
301
|
+
insertChannel(raw, {
|
|
302
|
+
id: "ch-email-1",
|
|
303
|
+
contactId: "c1",
|
|
304
|
+
type: "email",
|
|
305
|
+
address: "user@example.com",
|
|
306
|
+
externalUserId: null,
|
|
307
|
+
status: "active",
|
|
308
|
+
updatedAt: 1000,
|
|
309
|
+
});
|
|
310
|
+
insertChannel(raw, {
|
|
311
|
+
id: "ch-email-2",
|
|
312
|
+
contactId: "c1",
|
|
313
|
+
type: "email",
|
|
314
|
+
address: "user2@example.com",
|
|
315
|
+
externalUserId: null,
|
|
316
|
+
status: "active",
|
|
317
|
+
updatedAt: 1000,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
321
|
+
|
|
322
|
+
const channels = getAllChannels(raw);
|
|
323
|
+
expect(channels).toHaveLength(2);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("preserves external_user_id index (dropped by follow-up migration)", () => {
|
|
327
|
+
const db = createTestDb();
|
|
328
|
+
const raw = getSqliteFrom(db);
|
|
329
|
+
bootstrap(db);
|
|
330
|
+
|
|
331
|
+
const beforeIndexes = getIndexes(raw);
|
|
332
|
+
const oldIdx = beforeIndexes.find(
|
|
333
|
+
(i) => i.name === "idx_contact_channels_type_ext_user",
|
|
334
|
+
);
|
|
335
|
+
expect(oldIdx).toBeDefined();
|
|
336
|
+
|
|
337
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
338
|
+
|
|
339
|
+
const afterIndexes = getIndexes(raw);
|
|
340
|
+
// Index is preserved — lookups still use externalUserId until PR 2
|
|
341
|
+
expect(
|
|
342
|
+
afterIndexes.find((i) => i.name === "idx_contact_channels_type_ext_user"),
|
|
343
|
+
).toBeDefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("preserves existing case-sensitive UNIQUE(type, address) index", () => {
|
|
347
|
+
const db = createTestDb();
|
|
348
|
+
const raw = getSqliteFrom(db);
|
|
349
|
+
bootstrap(db);
|
|
350
|
+
|
|
351
|
+
insertContact(raw, "c1");
|
|
352
|
+
insertChannel(raw, {
|
|
353
|
+
id: "ch1",
|
|
354
|
+
contactId: "c1",
|
|
355
|
+
type: "slack",
|
|
356
|
+
address: "U789",
|
|
357
|
+
externalUserId: "U789",
|
|
358
|
+
status: "active",
|
|
359
|
+
updatedAt: 1000,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
363
|
+
|
|
364
|
+
// The unique index on (type, address) still exists
|
|
365
|
+
const afterIndexes = getIndexes(raw);
|
|
366
|
+
const typeAddrIdx = afterIndexes.find(
|
|
367
|
+
(i) => i.name === "idx_contact_channels_type_address",
|
|
368
|
+
);
|
|
369
|
+
expect(typeAddrIdx).toBeDefined();
|
|
370
|
+
expect(typeAddrIdx!.unique).toBe(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("same address on different channel types both survive", () => {
|
|
374
|
+
const db = createTestDb();
|
|
375
|
+
const raw = getSqliteFrom(db);
|
|
376
|
+
bootstrap(db);
|
|
377
|
+
|
|
378
|
+
insertContact(raw, "c1");
|
|
379
|
+
insertChannel(raw, {
|
|
380
|
+
id: "ch1",
|
|
381
|
+
contactId: "c1",
|
|
382
|
+
type: "slack",
|
|
383
|
+
address: "U111",
|
|
384
|
+
externalUserId: "U111",
|
|
385
|
+
status: "active",
|
|
386
|
+
updatedAt: 1000,
|
|
387
|
+
});
|
|
388
|
+
insertChannel(raw, {
|
|
389
|
+
id: "ch2",
|
|
390
|
+
contactId: "c1",
|
|
391
|
+
type: "telegram",
|
|
392
|
+
address: "U111",
|
|
393
|
+
externalUserId: "U111",
|
|
394
|
+
status: "active",
|
|
395
|
+
updatedAt: 1000,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
399
|
+
|
|
400
|
+
const channels = getAllChannels(raw);
|
|
401
|
+
expect(channels).toHaveLength(2);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("does not normalize address casing (deferred to follow-up migration)", () => {
|
|
405
|
+
const db = createTestDb();
|
|
406
|
+
const raw = getSqliteFrom(db);
|
|
407
|
+
bootstrap(db);
|
|
408
|
+
|
|
409
|
+
insertContact(raw, "c1");
|
|
410
|
+
// Simulates old write path that lowercased the address
|
|
411
|
+
insertChannel(raw, {
|
|
412
|
+
id: "ch1",
|
|
413
|
+
contactId: "c1",
|
|
414
|
+
type: "slack",
|
|
415
|
+
address: "u12345abc",
|
|
416
|
+
externalUserId: "U12345ABC",
|
|
417
|
+
status: "active",
|
|
418
|
+
updatedAt: 1000,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
422
|
+
|
|
423
|
+
const channels = getAllChannels(raw);
|
|
424
|
+
expect(channels).toHaveLength(1);
|
|
425
|
+
// Address is NOT normalized — casing restoration deferred to later migration
|
|
426
|
+
expect(channels[0]!.address).toBe("u12345abc");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("deduplicates rows sharing external_user_id but having different addresses", () => {
|
|
430
|
+
const db = createTestDb();
|
|
431
|
+
const raw = getSqliteFrom(db);
|
|
432
|
+
bootstrap(db);
|
|
433
|
+
|
|
434
|
+
insertContact(raw, "c1");
|
|
435
|
+
insertContact(raw, "c2");
|
|
436
|
+
|
|
437
|
+
// Two rows with different addresses but same external_user_id.
|
|
438
|
+
// Step 1 (address dedup) keeps both since addresses differ.
|
|
439
|
+
// Step 2 (external_user_id dedup) must reduce to one row to prevent
|
|
440
|
+
// step 3 from creating an address collision.
|
|
441
|
+
insertChannel(raw, {
|
|
442
|
+
id: "ch-old-addr",
|
|
443
|
+
contactId: "c1",
|
|
444
|
+
type: "slack",
|
|
445
|
+
address: "alice-old",
|
|
446
|
+
externalUserId: "U12345",
|
|
447
|
+
status: "unverified",
|
|
448
|
+
updatedAt: 500,
|
|
449
|
+
});
|
|
450
|
+
insertChannel(raw, {
|
|
451
|
+
id: "ch-correct",
|
|
452
|
+
contactId: "c2",
|
|
453
|
+
type: "slack",
|
|
454
|
+
address: "U12345",
|
|
455
|
+
externalUserId: "U12345",
|
|
456
|
+
status: "active",
|
|
457
|
+
updatedAt: 1000,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
461
|
+
|
|
462
|
+
const channels = getAllChannels(raw);
|
|
463
|
+
// Only one survives — active wins over unverified
|
|
464
|
+
expect(channels).toHaveLength(1);
|
|
465
|
+
expect(channels[0]!.id).toBe("ch-correct");
|
|
466
|
+
expect(channels[0]!.address).toBe("U12345");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("cross-column collision: row with NULL ext_user_id removed as blocker", () => {
|
|
470
|
+
const db = createTestDb();
|
|
471
|
+
const raw = getSqliteFrom(db);
|
|
472
|
+
bootstrap(db);
|
|
473
|
+
|
|
474
|
+
insertContact(raw, "c1");
|
|
475
|
+
insertContact(raw, "c2");
|
|
476
|
+
|
|
477
|
+
// Row A: has external_user_id but stale lowercased address
|
|
478
|
+
insertChannel(raw, {
|
|
479
|
+
id: "ch-normalizer",
|
|
480
|
+
contactId: "c1",
|
|
481
|
+
type: "slack",
|
|
482
|
+
address: "old-handle",
|
|
483
|
+
externalUserId: "U12345",
|
|
484
|
+
status: "active",
|
|
485
|
+
updatedAt: 2000,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Row B: occupies the target address with NULL external_user_id
|
|
489
|
+
insertChannel(raw, {
|
|
490
|
+
id: "ch-blocker",
|
|
491
|
+
contactId: "c2",
|
|
492
|
+
type: "slack",
|
|
493
|
+
address: "U12345",
|
|
494
|
+
externalUserId: null,
|
|
495
|
+
status: "unverified",
|
|
496
|
+
updatedAt: 1000,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
500
|
+
|
|
501
|
+
const channels = getAllChannels(raw);
|
|
502
|
+
// Blocker removed; normalizer keeps its address (normalization deferred)
|
|
503
|
+
expect(channels).toHaveLength(1);
|
|
504
|
+
expect(channels[0]!.id).toBe("ch-normalizer");
|
|
505
|
+
expect(channels[0]!.address).toBe("old-handle");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("cross-column collision: case-insensitive match removes lowercased blocker", () => {
|
|
509
|
+
const db = createTestDb();
|
|
510
|
+
const raw = getSqliteFrom(db);
|
|
511
|
+
bootstrap(db);
|
|
512
|
+
|
|
513
|
+
insertContact(raw, "c1");
|
|
514
|
+
insertContact(raw, "c2");
|
|
515
|
+
|
|
516
|
+
// Row A: has external_user_id with original Slack casing
|
|
517
|
+
insertChannel(raw, {
|
|
518
|
+
id: "ch-normalizer",
|
|
519
|
+
contactId: "c1",
|
|
520
|
+
type: "slack",
|
|
521
|
+
address: "old-handle",
|
|
522
|
+
externalUserId: "U12345ABC",
|
|
523
|
+
status: "active",
|
|
524
|
+
updatedAt: 2000,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Row B: occupies a lowercased variant of the target address (from old write paths)
|
|
528
|
+
insertChannel(raw, {
|
|
529
|
+
id: "ch-blocker",
|
|
530
|
+
contactId: "c2",
|
|
531
|
+
type: "slack",
|
|
532
|
+
address: "u12345abc",
|
|
533
|
+
externalUserId: null,
|
|
534
|
+
status: "unverified",
|
|
535
|
+
updatedAt: 1000,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
539
|
+
|
|
540
|
+
const channels = getAllChannels(raw);
|
|
541
|
+
// Blocker removed despite case difference — COLLATE NOCASE catches it
|
|
542
|
+
expect(channels).toHaveLength(1);
|
|
543
|
+
expect(channels[0]!.id).toBe("ch-normalizer");
|
|
544
|
+
expect(channels[0]!.address).toBe("old-handle");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("idempotent — safe to run twice", () => {
|
|
548
|
+
const db = createTestDb();
|
|
549
|
+
const raw = getSqliteFrom(db);
|
|
550
|
+
bootstrap(db);
|
|
551
|
+
|
|
552
|
+
insertContact(raw, "c1");
|
|
553
|
+
insertChannel(raw, {
|
|
554
|
+
id: "ch1",
|
|
555
|
+
contactId: "c1",
|
|
556
|
+
type: "slack",
|
|
557
|
+
address: "U999",
|
|
558
|
+
externalUserId: "U999",
|
|
559
|
+
status: "active",
|
|
560
|
+
updatedAt: 1000,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
564
|
+
migrateContactChannelsUniqueExtUser(db);
|
|
565
|
+
|
|
566
|
+
const channels = getAllChannels(raw);
|
|
567
|
+
expect(channels).toHaveLength(1);
|
|
568
|
+
// Original casing preserved
|
|
569
|
+
expect(channels[0]!.address).toBe("U999");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
@@ -272,6 +272,16 @@ export { migrateAddMemoryV3EverInjected } from "./277-add-memory-v3-ever-injecte
|
|
|
272
272
|
export { migrateToolInvocationsTelemetryColumns } from "./278-tool-invocations-telemetry-columns.js";
|
|
273
273
|
export { createSkillLoadedEventsTable } from "./279-create-skill-loaded-events.js";
|
|
274
274
|
export { migrateConversationsSurfacedAt } from "./280-conversations-surfaced-at.js";
|
|
275
|
+
export { migrateMemoryRetrospectiveRememberedLog } from "./281-memory-retrospective-remembered-log.js";
|
|
276
|
+
export { migrateScheduleInferenceProfile } from "./282-schedule-inference-profile.js";
|
|
277
|
+
export { migrateMemoryV3SelectionsMessageIdAndSections } from "./283-memory-v3-selections-message-id-and-sections.js";
|
|
278
|
+
export { migrateWorkflowRuns } from "./284-workflow-runs.js";
|
|
279
|
+
export { migrateScheduleWorkflowMode } from "./285-schedule-workflow-mode.js";
|
|
280
|
+
export { migrateWorkflowRunTrust } from "./286-workflow-run-trust.js";
|
|
281
|
+
export { migrateConversationOriginChannelIndex } from "./287-conversation-origin-channel-index.js";
|
|
282
|
+
export { migrateBackfillOriginChannelFromBindings } from "./288-backfill-origin-channel-from-bindings.js";
|
|
283
|
+
export { migrateContactChannelsUniqueExtUser } from "./289-contact-channels-unique-ext-user.js";
|
|
284
|
+
export { migrateScheduleCapabilities } from "./290-schedule-capabilities.js";
|
|
275
285
|
export {
|
|
276
286
|
MIGRATION_REGISTRY,
|
|
277
287
|
type MigrationRegistryEntry,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds preloaded apps into the workspace apps directory at daemon startup.
|
|
3
|
+
*
|
|
4
|
+
* A preloaded app ships as plain source files under
|
|
5
|
+
* `src/config/preloaded-apps/<dirName>/` and is copied into
|
|
6
|
+
* `data/apps/<dirName>/` with a fixed, well-known app id, then compiled.
|
|
7
|
+
* The assistant later populates it by overwriting designated content files
|
|
8
|
+
* (e.g. the personal page's `src/profile-data.ts`) and calling app_refresh —
|
|
9
|
+
* it never authors the layout itself.
|
|
10
|
+
*
|
|
11
|
+
* Seeding is idempotent and deliberately conservative: once the definition
|
|
12
|
+
* JSON exists the app is considered user-owned (its content may have been
|
|
13
|
+
* populated) and the template is never re-copied. A missing `dist/` triggers
|
|
14
|
+
* a recompile only.
|
|
15
|
+
*
|
|
16
|
+
* Seeding is gated on the activation-flow experiment: only assistants in the
|
|
17
|
+
* personal-page treatment arm get the app. The gate is evaluated once per
|
|
18
|
+
* daemon startup — an assistant moved into the arm later picks it up on its
|
|
19
|
+
* next restart, and an already-seeded app is never removed when the
|
|
20
|
+
* assistant leaves the arm (it is user-owned by then).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { cpSync, existsSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
import { compileApp } from "../bundler/app-compiler.js";
|
|
27
|
+
import {
|
|
28
|
+
getAssistantFeatureFlagValue,
|
|
29
|
+
initFeatureFlagOverrides,
|
|
30
|
+
} from "../config/assistant-feature-flags.js";
|
|
31
|
+
import type { AssistantConfig } from "../config/schema.js";
|
|
32
|
+
import { resolveBundledDir } from "../util/bundled-asset.js";
|
|
33
|
+
import { getLogger } from "../util/logger.js";
|
|
34
|
+
import { getAppsDir } from "./app-store.js";
|
|
35
|
+
|
|
36
|
+
const log = getLogger("preloaded-apps");
|
|
37
|
+
|
|
38
|
+
/** Well-known id (and dirName) of the personal landing page app. */
|
|
39
|
+
export const PERSONAL_PAGE_APP_ID = "personal-page";
|
|
40
|
+
|
|
41
|
+
/** Multivariate activation-flow experiment flag (scope "both"). */
|
|
42
|
+
export const ACTIVATION_FLOW_FLAG = "experiment-activation-flow-2026-06-03";
|
|
43
|
+
|
|
44
|
+
/** Arm of the activation-flow experiment that gets the preseeded page. */
|
|
45
|
+
export const PERSONAL_PAGE_ARM = "personal-page";
|
|
46
|
+
|
|
47
|
+
export async function seedPreloadedApps(
|
|
48
|
+
config: AssistantConfig,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
// The lifecycle hook fires this early in startup, possibly before the
|
|
51
|
+
// gateway override fetch has completed. Await it here (no-op when already
|
|
52
|
+
// cached; bounded retries otherwise) so the arm read below sees the
|
|
53
|
+
// platform-assigned value rather than the registry default.
|
|
54
|
+
await initFeatureFlagOverrides();
|
|
55
|
+
|
|
56
|
+
const arm = getAssistantFeatureFlagValue(ACTIVATION_FLOW_FLAG, config);
|
|
57
|
+
if (arm !== PERSONAL_PAGE_ARM) return;
|
|
58
|
+
|
|
59
|
+
await seedPersonalPageApp();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function seedPersonalPageApp(): Promise<void> {
|
|
63
|
+
const appsDir = getAppsDir();
|
|
64
|
+
const definitionPath = join(appsDir, `${PERSONAL_PAGE_APP_ID}.json`);
|
|
65
|
+
const appDir = join(appsDir, PERSONAL_PAGE_APP_ID);
|
|
66
|
+
const alreadySeeded = existsSync(definitionPath);
|
|
67
|
+
|
|
68
|
+
if (alreadySeeded && existsSync(join(appDir, "dist", "index.html"))) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!alreadySeeded) {
|
|
73
|
+
const templateDir = resolveBundledDir(
|
|
74
|
+
import.meta.dirname ?? __dirname,
|
|
75
|
+
"../config/preloaded-apps/personal-page",
|
|
76
|
+
"preloaded-apps/personal-page",
|
|
77
|
+
);
|
|
78
|
+
if (!existsSync(join(templateDir, "src"))) {
|
|
79
|
+
log.warn(
|
|
80
|
+
{ templateDir },
|
|
81
|
+
"Personal page template not found — skipping preloaded app seeding",
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cpSync(join(templateDir, "src"), join(appDir, "src"), { recursive: true });
|
|
87
|
+
// Multifile (formatVersion 2) apps keep an empty root index.html; the
|
|
88
|
+
// real entrypoint is src/index.html compiled into dist/. Matches the
|
|
89
|
+
// on-disk shape produced by createApp().
|
|
90
|
+
writeFileSync(join(appDir, "index.html"), "", "utf-8");
|
|
91
|
+
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const definition = {
|
|
94
|
+
id: PERSONAL_PAGE_APP_ID,
|
|
95
|
+
name: "Your Page",
|
|
96
|
+
description:
|
|
97
|
+
"A personal landing page the assistant fills in with what it learns about you.",
|
|
98
|
+
icon: "✨",
|
|
99
|
+
schemaJson: "{}",
|
|
100
|
+
createdAt: now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
formatVersion: 2,
|
|
103
|
+
dirName: PERSONAL_PAGE_APP_ID,
|
|
104
|
+
};
|
|
105
|
+
writeFileSync(definitionPath, JSON.stringify(definition, null, 2), "utf-8");
|
|
106
|
+
log.info({ appDir }, "Seeded personal page app");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = await compileApp(appDir);
|
|
110
|
+
if (!result.ok) {
|
|
111
|
+
log.warn(
|
|
112
|
+
{ errors: result.errors },
|
|
113
|
+
"Personal page app failed to compile after seeding",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|