@vellumai/assistant 0.10.2-dev.202606250318.5e7cfb0 → 0.10.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/bun.lock +0 -20
- package/docs/workspace-tools.md +33 -42
- package/eslint-rules/cli-no-daemon-internals.js +0 -6
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +0 -31
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +0 -44
- package/node_modules/@vellumai/gateway-client/src/index.ts +0 -14
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +0 -17
- package/node_modules/@vellumai/service-contracts/package.json +0 -1
- package/node_modules/@vellumai/service-contracts/src/index.ts +0 -1
- package/openapi.yaml +0 -155
- package/package.json +1 -4
- package/scripts/test.sh +15 -36
- package/src/__tests__/actor-token-service.test.ts +14 -36
- package/src/__tests__/agent-loop-override-profile.test.ts +0 -1
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -2
- package/src/__tests__/agent-wake-override-profile.test.ts +0 -2
- package/src/__tests__/annotate-activity-metadata.test.ts +0 -2
- package/src/__tests__/annotate-risk-options.test.ts +0 -2
- package/src/__tests__/approval-cascade.test.ts +0 -2
- package/src/__tests__/assistant-attachments.test.ts +0 -42
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -2
- package/src/__tests__/build-persisted-content.test.ts +0 -2
- package/src/__tests__/call-controller.test.ts +0 -19
- package/src/__tests__/channel-guardian.test.ts +58 -94
- package/src/__tests__/channel-reply-delivery.test.ts +0 -2
- package/src/__tests__/compaction-events.test.ts +0 -2
- package/src/__tests__/compaction.benchmark.test.ts +0 -2
- package/src/__tests__/compactor-call-site-logging.test.ts +0 -2
- package/src/__tests__/compactor-low-watermark-cut.test.ts +0 -2
- package/src/__tests__/compactor-preserved-tail-count.test.ts +0 -2
- package/src/__tests__/compactor-summary-call-truncation.test.ts +0 -2
- package/src/__tests__/compactor-web-search-strip.test.ts +0 -2
- package/src/__tests__/config-loader-backfill.test.ts +10 -123
- package/src/__tests__/config-schema.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -31
- package/src/__tests__/contacts-relay-reads.test.ts +15 -13
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +0 -134
- package/src/__tests__/conversation-analysis-routes.test.ts +0 -2
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +0 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -2
- package/src/__tests__/conversation-history-web-search.test.ts +0 -2
- package/src/__tests__/conversation-load-history-repair.test.ts +0 -2
- package/src/__tests__/conversation-load-history-stripped.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -2
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +0 -2
- package/src/__tests__/conversation-process-callsite.test.ts +0 -2
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -2
- package/src/__tests__/conversation-queue.test.ts +0 -91
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -14
- package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -14
- package/src/__tests__/conversation-slash-queue.test.ts +0 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -2
- package/src/__tests__/conversation-speed-override.test.ts +0 -2
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -29
- package/src/__tests__/conversation-title-service.test.ts +0 -2
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -47
- package/src/__tests__/conversation-usage.test.ts +0 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -2
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -2
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/db-migration-rollback.test.ts +171 -205
- package/src/__tests__/db-test-helpers.ts +4 -5
- package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -4
- package/src/__tests__/disk-pressure-guard.test.ts +0 -41
- package/src/__tests__/dm-persistence.test.ts +0 -2
- package/src/__tests__/emit-signal-routing-intent.test.ts +5 -10
- package/src/__tests__/events-dev-bypass-actor.test.ts +1 -7
- package/src/__tests__/exploration-drift-hook.test.ts +2 -3
- package/src/__tests__/filing-service.test.ts +0 -2
- package/src/__tests__/guardian-binding-drift-heal.test.ts +10 -75
- package/src/__tests__/guardian-dispatch.test.ts +1 -95
- package/src/__tests__/guardian-outbound-http.test.ts +0 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -2
- package/src/__tests__/heartbeat-service.test.ts +0 -2
- package/src/__tests__/helpers/channel-test-adapter.ts +7 -1
- package/src/__tests__/host-app-control-routes.test.ts +30 -24
- package/src/__tests__/host-bash-routes.test.ts +41 -31
- package/src/__tests__/host-browser-routes.test.ts +32 -26
- package/src/__tests__/host-cu-routes-targeted.test.ts +33 -25
- package/src/__tests__/host-file-routes-targeted.test.ts +52 -40
- package/src/__tests__/host-transfer-routes-targeted.test.ts +43 -31
- package/src/__tests__/http-user-message-parity.test.ts +8 -290
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -28
- package/src/__tests__/inbound-slack-persistence.test.ts +0 -2
- package/src/__tests__/invite-redemption-service.test.ts +0 -198
- package/src/__tests__/llm-context-normalization.test.ts +0 -105
- package/src/__tests__/llm-request-log-error-payload.test.ts +9 -71
- package/src/__tests__/llm-usage-store.test.ts +0 -25
- package/src/__tests__/mcp-health-check.test.ts +1 -2
- package/src/__tests__/media-stream-server-integration.test.ts +0 -127
- package/src/__tests__/memory-retrieval-hook.test.ts +0 -2
- package/src/__tests__/messaging-send-tool.test.ts +0 -2
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/mtime-cache.test.ts +5 -146
- package/src/__tests__/native-web-search.test.ts +0 -2
- package/src/__tests__/non-member-access-request.test.ts +17 -189
- package/src/__tests__/notification-broadcaster.test.ts +0 -4
- package/src/__tests__/notification-decision-recipient-context.test.ts +32 -33
- package/src/__tests__/notification-deep-link.test.ts +0 -6
- package/src/__tests__/notification-guardian-path.test.ts +0 -19
- package/src/__tests__/openai-provider.test.ts +12 -22
- package/src/__tests__/openai-responses-provider.test.ts +2 -12
- package/src/__tests__/outbound-slack-persistence.test.ts +0 -2
- package/src/__tests__/pending-interactions-resolved-event.test.ts +4 -7
- package/src/__tests__/persistence-secret-redaction.test.ts +0 -2
- package/src/__tests__/plugin-bootstrap.test.ts +73 -3
- package/src/__tests__/plugin-route-contribution.test.ts +17 -4
- package/src/__tests__/plugin-tool-contribution.test.ts +18 -3
- package/src/__tests__/plugin-types.test.ts +2 -0
- package/src/__tests__/process-message-background-slack.test.ts +0 -2
- package/src/__tests__/process-message-display-content.test.ts +0 -2
- package/src/__tests__/provider-error-scenarios.test.ts +4 -5
- package/src/__tests__/provider-usage-tracking.test.ts +0 -39
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -2
- package/src/__tests__/registry.test.ts +1 -4
- package/src/__tests__/relay-server.test.ts +25 -694
- package/src/__tests__/runtime-attachment-metadata.test.ts +1 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -14
- package/src/__tests__/send-endpoint-busy.test.ts +8 -30
- package/src/__tests__/skills.test.ts +0 -44
- package/src/__tests__/slack-inbound-verification.test.ts +2 -47
- package/src/__tests__/stt-hints.test.ts +13 -44
- package/src/__tests__/subagent-detail.test.ts +0 -27
- package/src/__tests__/subagent-disposal.test.ts +0 -65
- package/src/__tests__/subagent-notify-parent.test.ts +0 -2
- package/src/__tests__/subagent-role-registry.test.ts +2 -7
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +0 -2
- package/src/__tests__/suggestion-routes.test.ts +0 -2
- package/src/__tests__/title-generate-hook.test.ts +0 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -2
- package/src/__tests__/tool-executor.test.ts +11 -16
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -2
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +0 -2
- package/src/__tests__/tool-start-timestamp.test.ts +0 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +0 -96
- package/src/__tests__/ui-file-upload-surface.test.ts +0 -86
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-invite-redemption.test.ts +0 -33
- package/src/__tests__/web-search-backend-failure.test.ts +0 -2
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +35 -14
- package/src/__tests__/workspace-tool-loader.test.ts +2 -195
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +0 -56
- package/src/api/index.ts +1 -19
- package/src/api/responses/llm-request-log-entry.ts +0 -29
- package/src/api/responses/subagent-detail.ts +0 -17
- package/src/api/surfaces.ts +3 -39
- package/src/approvals/guardian-request-resolvers.ts +11 -1
- package/src/calls/__tests__/relay-setup-router.test.ts +4 -262
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +8 -10
- package/src/calls/inbound-trust-reader.ts +1 -17
- package/src/calls/media-stream-server.ts +0 -21
- package/src/calls/relay-server.ts +50 -167
- package/src/calls/relay-setup-router.ts +7 -37
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +12 -9
- package/src/calls/twilio-routes.ts +4 -14
- package/src/channels/types.ts +20 -10
- package/src/cli/commands/__tests__/cache.test.ts +1 -8
- package/src/cli/commands/cache.ts +181 -194
- package/src/cli/commands/db/__tests__/repair.test.ts +5 -6
- package/src/cli/commands/db/status.ts +1 -37
- package/src/cli/commands/mcp.ts +218 -252
- package/src/cli/commands/memory/index.ts +0 -2
- package/src/cli/commands/plugins.ts +3 -75
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -102
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +1 -160
- package/src/cli/lib/list-installed-plugins.ts +1 -179
- package/src/config/__tests__/sync-gated-profiles.test.ts +3 -11
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +17 -27
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +3 -13
- package/src/config/bundled-skills/subagent/SKILL.md +1 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
- package/src/config/feature-flag-registry.json +13 -5
- package/src/config/loader.ts +5 -38
- package/src/config/schemas/__tests__/memory-v3.test.ts +0 -1
- package/src/config/schemas/memory-lifecycle.ts +0 -12
- package/src/config/schemas/memory-v3.ts +0 -7
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/timeouts.ts +0 -8
- package/src/config/seed-inference-profiles.ts +11 -21
- package/src/config/skills.ts +5 -27
- package/src/config/sync-gated-profiles.ts +13 -12
- package/src/contacts/contacts-write.ts +0 -3
- package/src/daemon/assistant-attachments.ts +4 -27
- package/src/daemon/conversation-agent-loop.ts +0 -28
- package/src/daemon/conversation-process.ts +16 -35
- package/src/daemon/conversation-surfaces.ts +38 -111
- package/src/daemon/conversation-tool-setup.ts +16 -50
- package/src/daemon/conversation.ts +1 -13
- package/src/daemon/disk-pressure-guard.ts +2 -12
- package/src/daemon/event-loop-watchdog.ts +1 -28
- package/src/daemon/external-plugins-bootstrap.ts +34 -4
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -25
- package/src/daemon/handlers/config-a2a.ts +14 -6
- package/src/daemon/handlers/config-channels.ts +22 -78
- package/src/daemon/handlers/conversations.ts +0 -77
- package/src/daemon/lifecycle.ts +0 -4
- package/src/daemon/mcp-reload-service.ts +0 -10
- package/src/daemon/memory-v2-startup.test.ts +0 -72
- package/src/daemon/memory-v2-startup.ts +19 -87
- package/src/daemon/message-types/conversations.ts +0 -2
- package/src/daemon/message-types/surfaces.ts +12 -12
- package/src/daemon/server.ts +4 -0
- package/src/daemon/shutdown-handlers.ts +0 -20
- package/src/daemon/tool-setup-types.ts +0 -9
- package/src/daemon/workspace-tools-watcher.ts +328 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/client.ts +1 -15
- package/src/mcp/mcp-auth-orchestrator.ts +1 -6
- package/src/mcp/mcp-oauth-provider.ts +8 -19
- package/src/memory/__tests__/memory-retrospective-job.test.ts +0 -8
- package/src/memory/conversation-crud.ts +0 -38
- package/src/memory/db-connection.ts +3 -22
- package/src/memory/db-init.ts +502 -36
- package/src/memory/db-singleton.ts +4 -6
- package/src/memory/jobs-worker.ts +0 -58
- package/src/memory/llm-request-log-store.ts +1 -26
- package/src/memory/llm-usage-store.ts +20 -48
- package/src/memory/memory-retrospective-job.ts +8 -9
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +56 -130
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/registry.ts +573 -0
- package/src/memory/migrations/run-migrations.ts +6 -90
- package/src/memory/migrations/validate-migration-state.ts +66 -101
- package/src/memory/schema/conversations.ts +0 -9
- package/src/memory/schema/infrastructure.ts +0 -20
- package/src/memory/v2/__tests__/cli-command-store.test.ts +0 -25
- package/src/memory/v2/__tests__/skill-store.test.ts +0 -80
- package/src/memory/v2/cli-command-store.ts +38 -75
- package/src/memory/v2/prompts/consolidation.ts +82 -13
- package/src/memory/v2/prompts/router.ts +93 -21
- package/src/memory/v2/skill-store.ts +31 -68
- package/src/notifications/__tests__/broadcaster.test.ts +8 -16
- package/src/notifications/__tests__/decision-engine.test.ts +9 -78
- package/src/notifications/broadcaster.ts +1 -8
- package/src/notifications/decision-engine.ts +7 -15
- package/src/notifications/destination-resolver.ts +24 -68
- package/src/notifications/emit-signal.ts +14 -39
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +4 -7
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +5 -3
- package/src/plugin-api/vision-support.test.ts +4 -28
- package/src/plugin-api/vision-support.ts +31 -66
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -161
- package/src/plugins/defaults/advisor/consult.ts +6 -110
- package/src/plugins/defaults/advisor/steering.ts +2 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +5 -32
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +1 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +7 -47
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +11 -10
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +20 -12
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +11 -42
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +3 -33
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +4 -48
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +8 -4
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +15 -43
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +2 -11
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -77
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +11 -12
- package/src/plugins/mtime-cache.ts +291 -76
- package/src/plugins/pipeline.ts +13 -111
- package/src/plugins/types.ts +2 -0
- package/src/providers/anthropic/client.ts +0 -5
- package/src/providers/call-site-routing.ts +0 -4
- package/src/providers/model-catalog.ts +0 -16
- package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
- package/src/providers/openai/chat-completions-provider.ts +83 -37
- package/src/providers/openai/responses-provider.ts +46 -50
- package/src/providers/openrouter/client.ts +0 -5
- package/src/providers/provider-send-message.ts +0 -4
- package/src/providers/ratelimit.ts +0 -4
- package/src/providers/retry.ts +0 -4
- package/src/providers/types.ts +0 -9
- package/src/providers/usage-tracking.ts +0 -4
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +3 -335
- package/src/runtime/access-request-helper.ts +39 -19
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +2 -9
- package/src/runtime/auth/require-bound-guardian.ts +11 -21
- package/src/runtime/channel-verification-service.ts +31 -56
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +7 -66
- package/src/runtime/invite-redemption-service.ts +187 -198
- package/src/runtime/local-actor-identity.ts +11 -76
- package/src/runtime/pending-interactions.ts +1 -11
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +5 -56
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +0 -187
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +5 -13
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +32 -8
- package/src/runtime/routes/conversation-cli-routes.ts +5 -4
- package/src/runtime/routes/conversation-list-routes.ts +7 -4
- package/src/runtime/routes/conversation-query-routes.ts +0 -72
- package/src/runtime/routes/conversation-routes.ts +85 -84
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +1 -3
- package/src/runtime/routes/guardian-action-routes.ts +5 -4
- package/src/runtime/routes/host-app-control-routes.ts +4 -5
- package/src/runtime/routes/host-bash-routes.ts +4 -5
- package/src/runtime/routes/host-browser-routes.ts +11 -9
- package/src/runtime/routes/host-cu-routes.ts +4 -5
- package/src/runtime/routes/host-file-routes.ts +4 -5
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +2 -3
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +5 -97
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +49 -61
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -16
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +8 -21
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +3 -14
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/llm-context-normalization.ts +0 -83
- package/src/runtime/routes/mcp-auth-routes.ts +19 -171
- package/src/runtime/routes/migration-rollback-routes.ts +3 -4
- package/src/runtime/routes/migration-routes.ts +1 -4
- package/src/runtime/routes/subagents-routes.ts +0 -5
- package/src/runtime/routes/surface-action-routes.ts +56 -42
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -1
- package/src/runtime/services/conversation-serializer.ts +9 -7
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +9 -85
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +0 -16
- package/src/subagent/manager.ts +0 -9
- package/src/subagent/types.ts +3 -3
- package/src/telemetry/types.ts +1 -34
- package/src/telemetry/usage-telemetry-reporter.test.ts +2 -3
- package/src/telemetry/usage-telemetry-reporter.ts +3 -87
- package/src/tools/ask-question/ask-question-tool.test.ts +0 -29
- package/src/tools/ask-question/ask-question-tool.ts +0 -13
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +0 -18
- package/src/tools/shared/filesystem/path-policy.ts +5 -12
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +2 -9
- package/src/tools/tool-manifest.ts +0 -3
- package/src/tools/types.ts +2 -17
- package/src/tools/workspace-tools/loader.ts +244 -348
- package/src/util/errors.ts +1 -26
- package/src/util/platform.ts +0 -5
- package/src/workflows/library.test.ts +0 -140
- package/src/workflows/library.ts +28 -82
- package/src/workspace/migrations/017-seed-persona-dirs.ts +34 -3
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +24 -3
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +66 -14
- package/src/workspace/migrations/registry.ts +0 -2
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +0 -91
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +0 -48
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +0 -28
- package/node_modules/@vellumai/service-contracts/src/channels.ts +0 -41
- package/src/__tests__/code-search-tool.test.ts +0 -585
- package/src/__tests__/guardian-expiry-notifier.test.ts +0 -282
- package/src/__tests__/mcp-config-secret-boundary.test.ts +0 -390
- package/src/__tests__/plugin-pipeline.test.ts +0 -96
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +0 -102
- package/src/__tests__/steer-on-enqueue-question.test.ts +0 -181
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +0 -208
- package/src/agent/loop-exclusive-tool.test.ts +0 -150
- package/src/api/constants/sse-replay.ts +0 -41
- package/src/api/events/conversation-notice.ts +0 -26
- package/src/approvals/guardian-channel-delivery.ts +0 -30
- package/src/approvals/guardian-expiry-notifier.ts +0 -148
- package/src/cli/commands/memory/__tests__/worker.test.ts +0 -302
- package/src/cli/commands/memory/worker.ts +0 -175
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +0 -143
- package/src/config/prune-seeded-callsite-defaults.ts +0 -110
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -129
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +0 -312
- package/src/contacts/__tests__/member-write-relay.test.ts +0 -202
- package/src/contacts/guardian-delivery-reader.ts +0 -223
- package/src/contacts/member-write-relay.ts +0 -189
- package/src/daemon/conversation-notices.ts +0 -60
- package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -225
- package/src/hooks/hook-loader.ts +0 -341
- package/src/mcp/mcp-header-store.ts +0 -134
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +0 -110
- package/src/memory/__tests__/prompt-override.test.ts +0 -192
- package/src/memory/__tests__/watchdog-events-store.test.ts +0 -161
- package/src/memory/migrations/300-add-processing-started-at.ts +0 -30
- package/src/memory/migrations/301-create-watchdog-events.ts +0 -45
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +0 -224
- package/src/memory/prompt-override.ts +0 -129
- package/src/memory/steps.ts +0 -573
- package/src/memory/watchdog-events-store.ts +0 -87
- package/src/memory/worker-control.ts +0 -118
- package/src/memory/worker-process.ts +0 -72
- package/src/notifications/__tests__/connected-channels.test.ts +0 -114
- package/src/notifications/__tests__/destination-resolver.test.ts +0 -256
- package/src/onboarding/checkin-event.test.ts +0 -222
- package/src/onboarding/checkin-event.ts +0 -321
- package/src/onboarding/schedule-checkin.ts +0 -190
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +0 -146
- package/src/plugins/surface-import.ts +0 -121
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +0 -321
- package/src/providers/openai/api-error-normalization.ts +0 -270
- package/src/runtime/__tests__/channel-verification-service.test.ts +0 -133
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +0 -181
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +0 -66
- package/src/runtime/__tests__/local-principal-trust.test.ts +0 -164
- package/src/runtime/anchored-guardian.test.ts +0 -156
- package/src/runtime/anchored-guardian.ts +0 -135
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +0 -99
- package/src/runtime/local-principal-trust.ts +0 -52
- package/src/runtime/routes/__tests__/contact-routes.test.ts +0 -212
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -93
- package/src/runtime/routes/onboarding-checkin-routes.ts +0 -86
- package/src/tools/filesystem/search.ts +0 -543
- package/src/util/telemetry-db-path.ts +0 -24
- package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +0 -134
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildCheckinDescription,
|
|
5
|
-
buildCheckinTitle,
|
|
6
|
-
type BusyInterval,
|
|
7
|
-
checkinAvailabilityWindow,
|
|
8
|
-
chooseCheckinSlot,
|
|
9
|
-
extractBusyFromEvents,
|
|
10
|
-
findFirstOpenSlot,
|
|
11
|
-
type GcalEvent,
|
|
12
|
-
tomorrowInTimeZone,
|
|
13
|
-
zonedWallTimeToUtcMs,
|
|
14
|
-
} from "./checkin-event.js";
|
|
15
|
-
|
|
16
|
-
const TZ = "America/New_York";
|
|
17
|
-
const MIN = 60 * 1000;
|
|
18
|
-
const HOUR = 60 * MIN;
|
|
19
|
-
|
|
20
|
-
// A fixed "now": 2024-01-15 18:00 UTC (= 1pm EST). Tomorrow is 2024-01-16.
|
|
21
|
-
const NOW = Date.parse("2024-01-15T18:00:00Z");
|
|
22
|
-
|
|
23
|
-
/** Helper: epoch ms for tomorrow (2024-01-16) at a given EST hour. */
|
|
24
|
-
function estTomorrow(hour: number, minute = 0): number {
|
|
25
|
-
return zonedWallTimeToUtcMs(2024, 1, 16, hour, minute, TZ);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("buildCheckinTitle", () => {
|
|
29
|
-
test("both names", () => {
|
|
30
|
-
expect(
|
|
31
|
-
buildCheckinTitle({ userName: "Alex", assistantName: "Jarvis" }),
|
|
32
|
-
).toBe("Alex <> Jarvis: Day 2 Check-in");
|
|
33
|
-
});
|
|
34
|
-
test("user only", () => {
|
|
35
|
-
expect(buildCheckinTitle({ userName: "Alex" })).toBe(
|
|
36
|
-
"Alex: Day 2 Check-in",
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
test("assistant only", () => {
|
|
40
|
-
expect(buildCheckinTitle({ assistantName: "Jarvis" })).toBe(
|
|
41
|
-
"Jarvis: Day 2 Check-in",
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
test("neither (blank strings dropped)", () => {
|
|
45
|
-
expect(buildCheckinTitle({ userName: " ", assistantName: "" })).toBe(
|
|
46
|
-
"Day 2 Check-in",
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("buildCheckinDescription", () => {
|
|
52
|
-
test("embeds the uuid + fixed first-week prompt in the CTA link", () => {
|
|
53
|
-
const html = buildCheckinDescription("uuid-123");
|
|
54
|
-
expect(html).toContain(
|
|
55
|
-
"https://www.vellum.ai/assistant/conversations/uuid-123?prompt=What%20would%20you%20recommend",
|
|
56
|
-
);
|
|
57
|
-
// Carries onboarding attribution for the calendar-event CTA.
|
|
58
|
-
expect(html).toContain("&utm_source=onboarding&utm_medium=calendar_event");
|
|
59
|
-
// Only sanitization-safe tags; the CTA is a bold link, not a styled button.
|
|
60
|
-
expect(html).toContain("<a href=");
|
|
61
|
-
expect(html).toContain("<strong>");
|
|
62
|
-
expect(html).not.toContain("style=");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("findFirstOpenSlot", () => {
|
|
67
|
-
const windowStart = 0;
|
|
68
|
-
const windowEnd = 60 * MIN;
|
|
69
|
-
const dur = 15 * MIN;
|
|
70
|
-
|
|
71
|
-
test("empty calendar → start of window", () => {
|
|
72
|
-
expect(findFirstOpenSlot(windowStart, windowEnd, [], dur)).toBe(
|
|
73
|
-
windowStart,
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("busy at start → first gap after it", () => {
|
|
78
|
-
const busy: BusyInterval[] = [{ start: 0, end: 20 * MIN }];
|
|
79
|
-
expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBe(20 * MIN);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("finds a gap between two meetings", () => {
|
|
83
|
-
const busy: BusyInterval[] = [
|
|
84
|
-
{ start: 0, end: 10 * MIN },
|
|
85
|
-
{ start: 30 * MIN, end: 60 * MIN },
|
|
86
|
-
];
|
|
87
|
-
// gap [10,30) is 20min >= 15min → slot at 10min.
|
|
88
|
-
expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBe(10 * MIN);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("skips a gap too small to fit", () => {
|
|
92
|
-
const busy: BusyInterval[] = [
|
|
93
|
-
{ start: 0, end: 10 * MIN },
|
|
94
|
-
{ start: 20 * MIN, end: 60 * MIN }, // gap [10,20) only 10min
|
|
95
|
-
];
|
|
96
|
-
expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("fully booked → null", () => {
|
|
100
|
-
const busy: BusyInterval[] = [{ start: -MIN, end: windowEnd + MIN }];
|
|
101
|
-
expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("ignores out-of-window busy intervals", () => {
|
|
105
|
-
const busy: BusyInterval[] = [{ start: -2 * HOUR, end: -HOUR }];
|
|
106
|
-
expect(findFirstOpenSlot(windowStart, windowEnd, busy, dur)).toBe(
|
|
107
|
-
windowStart,
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("tomorrowInTimeZone", () => {
|
|
113
|
-
test("advances to the next local calendar day", () => {
|
|
114
|
-
expect(tomorrowInTimeZone(NOW, TZ)).toEqual({
|
|
115
|
-
year: 2024,
|
|
116
|
-
month: 1,
|
|
117
|
-
day: 16,
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("late-evening local time still maps to the correct next day", () => {
|
|
122
|
-
// 2024-01-16 02:00 UTC = 2024-01-15 21:00 EST → tomorrow is the 16th.
|
|
123
|
-
const lateLocal = Date.parse("2024-01-16T02:00:00Z");
|
|
124
|
-
expect(tomorrowInTimeZone(lateLocal, TZ)).toEqual({
|
|
125
|
-
year: 2024,
|
|
126
|
-
month: 1,
|
|
127
|
-
day: 16,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("chooseCheckinSlot", () => {
|
|
133
|
-
test("empty calendar → 12:00 (start of primary window)", () => {
|
|
134
|
-
const slot = chooseCheckinSlot(NOW, TZ, []);
|
|
135
|
-
expect(slot.window).toBe("primary");
|
|
136
|
-
expect(slot.startMs).toBe(estTomorrow(12));
|
|
137
|
-
expect(slot.endMs).toBe(estTomorrow(12, 15));
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("noon booked → next open slot inside the primary window", () => {
|
|
141
|
-
const busy: BusyInterval[] = [
|
|
142
|
-
{ start: estTomorrow(12), end: estTomorrow(13) },
|
|
143
|
-
];
|
|
144
|
-
const slot = chooseCheckinSlot(NOW, TZ, busy);
|
|
145
|
-
expect(slot.window).toBe("primary");
|
|
146
|
-
expect(slot.startMs).toBe(estTomorrow(13));
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("primary window full → widens to 8am–8pm and takes earliest slot", () => {
|
|
150
|
-
// Block all of 12pm–5pm; leave the morning open.
|
|
151
|
-
const busy: BusyInterval[] = [
|
|
152
|
-
{ start: estTomorrow(12), end: estTomorrow(17) },
|
|
153
|
-
];
|
|
154
|
-
const slot = chooseCheckinSlot(NOW, TZ, busy);
|
|
155
|
-
expect(slot.window).toBe("wide");
|
|
156
|
-
// Earliest free slot in 8am–8pm is 8:00am.
|
|
157
|
-
expect(slot.startMs).toBe(estTomorrow(8));
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("entire 8am–8pm full → fallback books at noon anyway", () => {
|
|
161
|
-
const busy: BusyInterval[] = [
|
|
162
|
-
{ start: estTomorrow(8), end: estTomorrow(20) },
|
|
163
|
-
];
|
|
164
|
-
const slot = chooseCheckinSlot(NOW, TZ, busy);
|
|
165
|
-
expect(slot.window).toBe("fallback");
|
|
166
|
-
expect(slot.startMs).toBe(estTomorrow(12));
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe("extractBusyFromEvents", () => {
|
|
171
|
-
const a = "2024-01-16T13:00:00Z";
|
|
172
|
-
const b = "2024-01-16T13:30:00Z";
|
|
173
|
-
|
|
174
|
-
test("timed event → busy interval", () => {
|
|
175
|
-
const events: GcalEvent[] = [
|
|
176
|
-
{ start: { dateTime: a }, end: { dateTime: b } },
|
|
177
|
-
];
|
|
178
|
-
expect(extractBusyFromEvents(events)).toEqual([
|
|
179
|
-
{ start: Date.parse(a), end: Date.parse(b) },
|
|
180
|
-
]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("skips cancelled, transparent, all-day, and declined events", () => {
|
|
184
|
-
const events: GcalEvent[] = [
|
|
185
|
-
{ status: "cancelled", start: { dateTime: a }, end: { dateTime: b } },
|
|
186
|
-
{
|
|
187
|
-
transparency: "transparent",
|
|
188
|
-
start: { dateTime: a },
|
|
189
|
-
end: { dateTime: b },
|
|
190
|
-
},
|
|
191
|
-
{ start: { date: "2024-01-16" }, end: { date: "2024-01-17" } },
|
|
192
|
-
{
|
|
193
|
-
start: { dateTime: a },
|
|
194
|
-
end: { dateTime: b },
|
|
195
|
-
attendees: [{ self: true, responseStatus: "declined" }],
|
|
196
|
-
},
|
|
197
|
-
];
|
|
198
|
-
expect(extractBusyFromEvents(events)).toEqual([]);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("keeps an accepted event even with another attendee declined", () => {
|
|
202
|
-
const events: GcalEvent[] = [
|
|
203
|
-
{
|
|
204
|
-
start: { dateTime: a },
|
|
205
|
-
end: { dateTime: b },
|
|
206
|
-
attendees: [
|
|
207
|
-
{ self: true, responseStatus: "accepted" },
|
|
208
|
-
{ responseStatus: "declined" },
|
|
209
|
-
],
|
|
210
|
-
},
|
|
211
|
-
];
|
|
212
|
-
expect(extractBusyFromEvents(events)).toHaveLength(1);
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe("checkinAvailabilityWindow", () => {
|
|
217
|
-
test("spans the full 8am–8pm fallback window tomorrow", () => {
|
|
218
|
-
const { timeMinMs, timeMaxMs } = checkinAvailabilityWindow(NOW, TZ);
|
|
219
|
-
expect(timeMinMs).toBe(estTomorrow(8));
|
|
220
|
-
expect(timeMaxMs).toBe(estTomorrow(20));
|
|
221
|
-
});
|
|
222
|
-
});
|
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for the onboarding "Day 2 Check-in" meeting: choosing the slot
|
|
3
|
-
* and building the event's title and HTML description.
|
|
4
|
-
*
|
|
5
|
-
* The check-in is booked into the first open 15-minute slot in the user's local
|
|
6
|
-
* afternoon tomorrow. The title uses fixed typography and the description uses a
|
|
7
|
-
* sanitization-safe HTML body (Google Calendar strips most styling) with
|
|
8
|
-
* default first-run substitutions and a deep-link CTA back into the app.
|
|
9
|
-
*
|
|
10
|
-
* Everything here is side-effect-free and timezone-pure so it can be unit
|
|
11
|
-
* tested without a calendar connection; the orchestration that talks to Google
|
|
12
|
-
* lives in `schedule-checkin.ts`.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** Length of the check-in meeting. */
|
|
16
|
-
export const CHECKIN_DURATION_MINUTES = 15;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Primary booking window in the user's local clock: the first open 15-minute
|
|
20
|
-
* slot between 12pm and 5pm tomorrow.
|
|
21
|
-
*/
|
|
22
|
-
export const PRIMARY_WINDOW = { startHour: 12, endHour: 17 } as const;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Fallback window when the primary 12pm–5pm window is fully booked: widen on
|
|
26
|
-
* both ends to 8am–8pm and take the earliest open slot there.
|
|
27
|
-
*/
|
|
28
|
-
export const WIDE_WINDOW = { startHour: 8, endHour: 20 } as const;
|
|
29
|
-
|
|
30
|
-
/** The fixed, single-encoded first-week prompt carried by the CTA deep link. */
|
|
31
|
-
const CTA_ENCODED_PROMPT =
|
|
32
|
-
"What%20would%20you%20recommend%20I%20tackle%20first%20this%20week%3F%20Propose%20it%20but%20wait%20for%20my%20go-ahead%20before%20doing%20anything";
|
|
33
|
-
|
|
34
|
-
export interface CheckinNames {
|
|
35
|
-
/** The user's collected name. Blank/omitted → dropped from the title. */
|
|
36
|
-
userName?: string;
|
|
37
|
-
/** The assistant's display name. Blank/omitted → dropped from the title. */
|
|
38
|
-
assistantName?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Build the check-in event title. Locked typography, four cases:
|
|
43
|
-
* both names → `{me} <> {you}: Day 2 Check-in`
|
|
44
|
-
* me only → `{me}: Day 2 Check-in`
|
|
45
|
-
* you only → `{you}: Day 2 Check-in`
|
|
46
|
-
* neither → `Day 2 Check-in`
|
|
47
|
-
*/
|
|
48
|
-
export function buildCheckinTitle({
|
|
49
|
-
userName,
|
|
50
|
-
assistantName,
|
|
51
|
-
}: CheckinNames): string {
|
|
52
|
-
const me = userName?.trim();
|
|
53
|
-
const you = assistantName?.trim();
|
|
54
|
-
if (me && you) return `${me} <> ${you}: Day 2 Check-in`;
|
|
55
|
-
if (me) return `${me}: Day 2 Check-in`;
|
|
56
|
-
if (you) return `${you}: Day 2 Check-in`;
|
|
57
|
-
return "Day 2 Check-in";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Build the HTML event description with the default first-run substitutions.
|
|
62
|
-
*
|
|
63
|
-
* Google Calendar strips nearly all styling, so the body relies only on the
|
|
64
|
-
* tags that survive (`<p>`, `<strong>`, `<a>`, emoji) and makes the CTA a bold
|
|
65
|
-
* link rather than a styled button. The deep link opens a fresh conversation
|
|
66
|
-
* (`uuid`) pre-seeded with the first-week prompt.
|
|
67
|
-
*/
|
|
68
|
-
export function buildCheckinDescription(uuid: string): string {
|
|
69
|
-
const href = `https://www.vellum.ai/assistant/conversations/${uuid}?prompt=${CTA_ENCODED_PROMPT}&utm_source=onboarding&utm_medium=calendar_event`;
|
|
70
|
-
return [
|
|
71
|
-
"<p>👋 <strong>Hi, it was great to meet you properly.</strong></p>",
|
|
72
|
-
"<p>You just set me up, and I've already started learning <strong>what you're working on</strong>. This 15 minutes is the natural place to put that to work. I'll walk you through one thing I'd like to do for you this week.</p>",
|
|
73
|
-
`<p><a href="${href}"><strong>Let's go →</strong></a></p>`,
|
|
74
|
-
"<p>Click the link and we'll get started.</p>",
|
|
75
|
-
].join("\n");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Timezone math
|
|
80
|
-
//
|
|
81
|
-
// We resolve wall-clock windows ("tomorrow 12:00 in America/New_York") to
|
|
82
|
-
// absolute instants using Intl, so DST transitions and arbitrary offsets are
|
|
83
|
-
// handled without a date library.
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Offset (local − UTC, in ms) that `timeZone` has at the given instant.
|
|
88
|
-
* Derived by formatting the instant in the zone and diffing the wall clock.
|
|
89
|
-
*/
|
|
90
|
-
function tzOffsetMs(utcMs: number, timeZone: string): number {
|
|
91
|
-
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
92
|
-
timeZone,
|
|
93
|
-
hour12: false,
|
|
94
|
-
year: "numeric",
|
|
95
|
-
month: "2-digit",
|
|
96
|
-
day: "2-digit",
|
|
97
|
-
hour: "2-digit",
|
|
98
|
-
minute: "2-digit",
|
|
99
|
-
second: "2-digit",
|
|
100
|
-
});
|
|
101
|
-
const map: Record<string, string> = {};
|
|
102
|
-
for (const part of dtf.formatToParts(new Date(utcMs))) {
|
|
103
|
-
if (part.type !== "literal") map[part.type] = part.value;
|
|
104
|
-
}
|
|
105
|
-
// `hourCycle: h23` can emit "24" for midnight on some runtimes — normalize.
|
|
106
|
-
let hour = Number(map.hour);
|
|
107
|
-
if (hour === 24) hour = 0;
|
|
108
|
-
const asUtc = Date.UTC(
|
|
109
|
-
Number(map.year),
|
|
110
|
-
Number(map.month) - 1,
|
|
111
|
-
Number(map.day),
|
|
112
|
-
hour,
|
|
113
|
-
Number(map.minute),
|
|
114
|
-
Number(map.second),
|
|
115
|
-
);
|
|
116
|
-
return asUtc - utcMs;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Convert a wall-clock time in `timeZone` to an absolute epoch-ms instant.
|
|
121
|
-
* Refines once to settle DST boundaries where the naive offset guess differs
|
|
122
|
-
* from the offset that actually applies at the resolved instant.
|
|
123
|
-
*/
|
|
124
|
-
export function zonedWallTimeToUtcMs(
|
|
125
|
-
year: number,
|
|
126
|
-
month: number,
|
|
127
|
-
day: number,
|
|
128
|
-
hour: number,
|
|
129
|
-
minute: number,
|
|
130
|
-
timeZone: string,
|
|
131
|
-
): number {
|
|
132
|
-
const guess = Date.UTC(year, month - 1, day, hour, minute);
|
|
133
|
-
const off1 = tzOffsetMs(guess, timeZone);
|
|
134
|
-
let utc = guess - off1;
|
|
135
|
-
const off2 = tzOffsetMs(utc, timeZone);
|
|
136
|
-
if (off2 !== off1) utc = guess - off2;
|
|
137
|
-
return utc;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Calendar date (1-based month) that is "tomorrow" in `timeZone` at `nowMs`. */
|
|
141
|
-
export function tomorrowInTimeZone(
|
|
142
|
-
nowMs: number,
|
|
143
|
-
timeZone: string,
|
|
144
|
-
): { year: number; month: number; day: number } {
|
|
145
|
-
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
146
|
-
timeZone,
|
|
147
|
-
year: "numeric",
|
|
148
|
-
month: "2-digit",
|
|
149
|
-
day: "2-digit",
|
|
150
|
-
});
|
|
151
|
-
const map: Record<string, string> = {};
|
|
152
|
-
for (const part of dtf.formatToParts(new Date(nowMs))) {
|
|
153
|
-
if (part.type !== "literal") map[part.type] = part.value;
|
|
154
|
-
}
|
|
155
|
-
// Add a day via UTC arithmetic (handles month/year rollover), then read the
|
|
156
|
-
// wall-clock date back out — we only care about Y/M/D, not the instant.
|
|
157
|
-
const tomorrow = new Date(
|
|
158
|
-
Date.UTC(Number(map.year), Number(map.month) - 1, Number(map.day)) +
|
|
159
|
-
24 * 60 * 60 * 1000,
|
|
160
|
-
);
|
|
161
|
-
return {
|
|
162
|
-
year: tomorrow.getUTCFullYear(),
|
|
163
|
-
month: tomorrow.getUTCMonth() + 1,
|
|
164
|
-
day: tomorrow.getUTCDate(),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// Slot finding
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
/** A half-open busy interval `[start, end)` in epoch ms. */
|
|
173
|
-
export interface BusyInterval {
|
|
174
|
-
start: number;
|
|
175
|
-
end: number;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Subset of a Google Calendar event needed to decide whether it blocks time. */
|
|
179
|
-
export interface GcalEvent {
|
|
180
|
-
status?: string;
|
|
181
|
-
transparency?: string;
|
|
182
|
-
start?: { dateTime?: string; date?: string };
|
|
183
|
-
end?: { dateTime?: string; date?: string };
|
|
184
|
-
attendees?: Array<{ self?: boolean; responseStatus?: string }>;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Derive busy intervals (epoch ms) from listed calendar events, approximating
|
|
189
|
-
* freeBusy semantics under the calendar.events scope (which authorizes
|
|
190
|
-
* events.list/insert but not freeBusy.query). Skips events that don't occupy
|
|
191
|
-
* the user's time: cancelled, transparent ("free"), all-day (date-only), and
|
|
192
|
-
* ones the user has declined.
|
|
193
|
-
*/
|
|
194
|
-
export function extractBusyFromEvents(events: GcalEvent[]): BusyInterval[] {
|
|
195
|
-
const intervals: BusyInterval[] = [];
|
|
196
|
-
for (const event of events) {
|
|
197
|
-
if (event.status === "cancelled") continue;
|
|
198
|
-
if (event.transparency === "transparent") continue;
|
|
199
|
-
// All-day events (date-only) don't block a 15-minute afternoon slot.
|
|
200
|
-
if (!event.start?.dateTime || !event.end?.dateTime) continue;
|
|
201
|
-
const self = event.attendees?.find((a) => a.self);
|
|
202
|
-
if (self?.responseStatus === "declined") continue;
|
|
203
|
-
|
|
204
|
-
const start = Date.parse(event.start.dateTime);
|
|
205
|
-
const end = Date.parse(event.end.dateTime);
|
|
206
|
-
if (Number.isFinite(start) && Number.isFinite(end) && end > start) {
|
|
207
|
-
intervals.push({ start, end });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return intervals;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Earliest start (epoch ms) of a free `durationMs` slot inside
|
|
215
|
-
* `[windowStart, windowEnd)`, or `null` if the window is fully booked.
|
|
216
|
-
*
|
|
217
|
-
* Walks the busy intervals in order, advancing a cursor past each overlap; the
|
|
218
|
-
* first gap wide enough to fit the duration wins. This returns the *first*
|
|
219
|
-
* open slot, which is exactly the "first open time slot in the window"
|
|
220
|
-
* behavior the onboarding flow wants.
|
|
221
|
-
*/
|
|
222
|
-
export function findFirstOpenSlot(
|
|
223
|
-
windowStart: number,
|
|
224
|
-
windowEnd: number,
|
|
225
|
-
busy: BusyInterval[],
|
|
226
|
-
durationMs: number,
|
|
227
|
-
): number | null {
|
|
228
|
-
const relevant = busy
|
|
229
|
-
.filter((b) => b.end > windowStart && b.start < windowEnd)
|
|
230
|
-
.sort((a, b) => a.start - b.start);
|
|
231
|
-
|
|
232
|
-
let cursor = windowStart;
|
|
233
|
-
for (const interval of relevant) {
|
|
234
|
-
if (interval.start - cursor >= durationMs) {
|
|
235
|
-
return cursor;
|
|
236
|
-
}
|
|
237
|
-
if (interval.end > cursor) {
|
|
238
|
-
cursor = interval.end;
|
|
239
|
-
}
|
|
240
|
-
if (cursor + durationMs > windowEnd) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return cursor + durationMs <= windowEnd ? cursor : null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export interface ChosenSlot {
|
|
248
|
-
/** Slot start, epoch ms. */
|
|
249
|
-
startMs: number;
|
|
250
|
-
/** Slot end, epoch ms. */
|
|
251
|
-
endMs: number;
|
|
252
|
-
/** Which window the slot came from — drives logging/telemetry only. */
|
|
253
|
-
window: "primary" | "wide" | "fallback";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Pick the check-in slot for tomorrow in `timeZone` given the busy intervals.
|
|
258
|
-
*
|
|
259
|
-
* 1. First open 15-min slot in 12pm–5pm.
|
|
260
|
-
* 2. If that window is full, widen to 8am–8pm and take the earliest open slot.
|
|
261
|
-
* 3. If even that is full (pathological), fall back to 12:00 so a reminder
|
|
262
|
-
* still lands — better an overlapping check-in than none.
|
|
263
|
-
*/
|
|
264
|
-
export function chooseCheckinSlot(
|
|
265
|
-
nowMs: number,
|
|
266
|
-
timeZone: string,
|
|
267
|
-
busy: BusyInterval[],
|
|
268
|
-
): ChosenSlot {
|
|
269
|
-
const { year, month, day } = tomorrowInTimeZone(nowMs, timeZone);
|
|
270
|
-
const durationMs = CHECKIN_DURATION_MINUTES * 60 * 1000;
|
|
271
|
-
|
|
272
|
-
const at = (hour: number) =>
|
|
273
|
-
zonedWallTimeToUtcMs(year, month, day, hour, 0, timeZone);
|
|
274
|
-
|
|
275
|
-
const primaryStart = at(PRIMARY_WINDOW.startHour);
|
|
276
|
-
const primaryEnd = at(PRIMARY_WINDOW.endHour);
|
|
277
|
-
const wideStart = at(WIDE_WINDOW.startHour);
|
|
278
|
-
const wideEnd = at(WIDE_WINDOW.endHour);
|
|
279
|
-
|
|
280
|
-
const primary = findFirstOpenSlot(primaryStart, primaryEnd, busy, durationMs);
|
|
281
|
-
if (primary !== null) {
|
|
282
|
-
return { startMs: primary, endMs: primary + durationMs, window: "primary" };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const wide = findFirstOpenSlot(wideStart, wideEnd, busy, durationMs);
|
|
286
|
-
if (wide !== null) {
|
|
287
|
-
return { startMs: wide, endMs: wide + durationMs, window: "wide" };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
startMs: primaryStart,
|
|
292
|
-
endMs: primaryStart + durationMs,
|
|
293
|
-
window: "fallback",
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Window covering both the primary and fallback ranges — the availability query span. */
|
|
298
|
-
export function checkinAvailabilityWindow(
|
|
299
|
-
nowMs: number,
|
|
300
|
-
timeZone: string,
|
|
301
|
-
): { timeMinMs: number; timeMaxMs: number } {
|
|
302
|
-
const { year, month, day } = tomorrowInTimeZone(nowMs, timeZone);
|
|
303
|
-
return {
|
|
304
|
-
timeMinMs: zonedWallTimeToUtcMs(
|
|
305
|
-
year,
|
|
306
|
-
month,
|
|
307
|
-
day,
|
|
308
|
-
WIDE_WINDOW.startHour,
|
|
309
|
-
0,
|
|
310
|
-
timeZone,
|
|
311
|
-
),
|
|
312
|
-
timeMaxMs: zonedWallTimeToUtcMs(
|
|
313
|
-
year,
|
|
314
|
-
month,
|
|
315
|
-
day,
|
|
316
|
-
WIDE_WINDOW.endHour,
|
|
317
|
-
0,
|
|
318
|
-
timeZone,
|
|
319
|
-
),
|
|
320
|
-
};
|
|
321
|
-
}
|