@vellumai/assistant 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +13 -19
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +17 -0
- package/docker-init-apt-root.sh +167 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +642 -5
- package/package.json +3 -1
- package/scripts/generate-openapi.ts +83 -10
- package/scripts/sync-llm-catalog.ts +2 -2
- package/scripts/sync-web-search-catalog.ts +47 -25
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +45 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/config-schema.test.ts +21 -0
- package/src/__tests__/config-set-route.test.ts +80 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/context-search-conversations-source.test.ts +117 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +31 -65
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
- package/src/__tests__/conversation-agent-loop.test.ts +59 -1
- package/src/__tests__/conversation-error.test.ts +42 -3
- package/src/__tests__/conversation-fork-crud.test.ts +82 -0
- package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
- package/src/__tests__/conversation-lifecycle.test.ts +173 -0
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +4 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +3 -2
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +151 -55
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +73 -0
- package/src/__tests__/host-app-control-proxy.test.ts +507 -10
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
- package/src/__tests__/inference-profile-reaper.test.ts +4 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
- package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +15 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +58 -13
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +41 -29
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/oauth-commands-routes.test.ts +168 -16
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/openai-provider.test.ts +242 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +10 -36
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +0 -1
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
- package/src/__tests__/plugin-types.test.ts +3 -13
- package/src/__tests__/process-message-background-slack.test.ts +8 -1
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-routes.test.ts +50 -3
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
- package/src/__tests__/skill-load-tool.test.ts +27 -89
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/system-prompt.test.ts +670 -63
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/user-plugin-loader.test.ts +0 -7
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/agent/loop.ts +167 -18
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +122 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +960 -0
- package/src/cli/commands/changelog.ts +106 -42
- package/src/cli/commands/conversations.ts +102 -17
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/notifications.ts +388 -346
- package/src/cli/commands/plugins.ts +252 -0
- package/src/cli/commands/schedules.ts +683 -0
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/install-from-github.ts +303 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +52 -2
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -0
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +41 -9
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +9 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +17 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +23 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1107 -0
- package/src/context/token-estimator.ts +34 -36
- package/src/context/window-manager.ts +197 -1520
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
- package/src/daemon/approval-generators.ts +8 -6
- package/src/daemon/config-watcher.ts +94 -31
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +198 -11
- package/src/daemon/conversation-error.ts +171 -37
- package/src/daemon/conversation-lifecycle.ts +53 -40
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +49 -12
- package/src/daemon/conversation-runtime-assembly.ts +25 -1
- package/src/daemon/conversation-slash.ts +12 -5
- package/src/daemon/conversation-store.ts +11 -4
- package/src/daemon/conversation-tool-setup.ts +39 -7
- package/src/daemon/conversation.ts +33 -8
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +111 -108
- package/src/daemon/history-repair.ts +28 -1
- package/src/daemon/host-app-control-proxy.ts +153 -27
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +89 -91
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +85 -0
- package/src/daemon/message-protocol.ts +1 -0
- package/src/daemon/message-types/conversations.ts +25 -0
- package/src/daemon/message-types/messages.ts +61 -0
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pkb-reminder-builder.test.ts +11 -54
- package/src/daemon/pkb-reminder-builder.ts +5 -20
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +24 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +33 -0
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +73 -189
- package/src/home/__tests__/feed-types.test.ts +80 -0
- package/src/home/feed-types.ts +36 -2
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/ipc/cli-client.ts +147 -45
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
- package/src/memory/__tests__/conversation-queries.test.ts +483 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +42 -10
- package/src/memory/context-search/sources/conversations.ts +62 -2
- package/src/memory/context-search/sources/workspace.ts +4 -0
- package/src/memory/conversation-crud.ts +63 -19
- package/src/memory/conversation-queries.ts +197 -11
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +12 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +4 -4
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
- package/src/memory/graph/conversation-graph-memory.ts +49 -21
- package/src/memory/graph/tools.ts +9 -40
- package/src/memory/indexer.ts +34 -29
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
- package/src/memory/jobs/embed-concept-page.ts +20 -11
- package/src/memory/jobs-worker.ts +6 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
- package/src/memory/message-content.ts +1 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +9 -0
- package/src/memory/migrations/registry.ts +16 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +3 -3
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +288 -11
- package/src/memory/v2/__tests__/migration.test.ts +87 -0
- package/src/memory/v2/__tests__/page-index.test.ts +83 -0
- package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
- package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
- package/src/memory/v2/__tests__/router.test.ts +15 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +81 -26
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +63 -8
- package/src/memory/v2/prompts/router.ts +11 -8
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +135 -7
- package/src/memory/v2/router.ts +9 -8
- package/src/memory/v2/skill-store.ts +120 -35
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +113 -45
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +21 -1
- package/src/notifications/home-feed-side-effect.ts +138 -5
- package/src/notifications/signal.ts +3 -5
- package/src/notifications/types.ts +8 -0
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +19 -6
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +34 -1
- package/src/plugin-api/types.ts +104 -22
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +74 -22
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +187 -42
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +40 -26
- package/src/plugins/user-loader.ts +21 -2
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +37 -5
- package/src/prompts/__tests__/system-prompt.test.ts +10 -43
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -174
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +164 -0
- package/src/providers/__tests__/inference.test.ts +24 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +68 -11
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +32 -6
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +159 -34
- package/src/providers/inference/resolve-auth.ts +14 -4
- package/src/providers/model-catalog.ts +249 -12
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +169 -8
- package/src/providers/openrouter/client.ts +49 -4
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +38 -0
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +52 -15
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +103 -15
- package/src/runtime/auth/route-policy.ts +21 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +19 -47
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +5 -3
- package/src/runtime/routes/auth-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +5 -3
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +126 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-cli-routes.ts +44 -3
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +17 -42
- package/src/runtime/routes/conversation-query-routes.ts +99 -35
- package/src/runtime/routes/conversation-routes.ts +97 -11
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
- package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
- package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +6 -13
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/notification-routes.ts +1 -1
- package/src/runtime/routes/oauth-commands-routes.ts +105 -15
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +4 -7
- package/src/runtime/routes/subagents-routes.ts +98 -18
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +27 -2
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +93 -1
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +9 -1
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +17 -7
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/browser-execution.ts +15 -11
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +107 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/watcher/__tests__/engine.test.ts +22 -0
- package/src/watcher/engine.ts +6 -2
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/migrations/runner.ts +39 -9
- package/src/workspace/migrations/types.ts +4 -0
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-window-manager.test.ts +0 -2481
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -47,12 +47,24 @@ mock.module("../../util/platform.js", () => ({
|
|
|
47
47
|
|
|
48
48
|
// Stub config so heartbeat is enabled. Must export every symbol from
|
|
49
49
|
// the real module because Bun's mock.module replaces the entire module.
|
|
50
|
-
|
|
50
|
+
// Tests that need to flex maxConsecutiveRuns mutate this in-place.
|
|
51
|
+
const stubConfig: {
|
|
52
|
+
heartbeat: {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
intervalMs: number;
|
|
55
|
+
activeHoursStart: number | null;
|
|
56
|
+
activeHoursEnd: number | null;
|
|
57
|
+
maxConsecutiveRuns: number | null;
|
|
58
|
+
disposition: string;
|
|
59
|
+
};
|
|
60
|
+
} = {
|
|
51
61
|
heartbeat: {
|
|
52
62
|
enabled: true,
|
|
53
63
|
intervalMs: 60_000,
|
|
54
64
|
activeHoursStart: null,
|
|
55
65
|
activeHoursEnd: null,
|
|
66
|
+
maxConsecutiveRuns: null,
|
|
67
|
+
disposition: "Default disposition text.",
|
|
56
68
|
},
|
|
57
69
|
};
|
|
58
70
|
mock.module("../../config/loader.js", () => ({
|
|
@@ -91,7 +103,6 @@ mock.module("../../prompts/system-prompt.js", () => ({
|
|
|
91
103
|
SYSTEM_PROMPT_CACHE_BOUNDARY: "<<CACHE_BOUNDARY>>",
|
|
92
104
|
buildCoreIdentityContext: () => "",
|
|
93
105
|
buildSystemPrompt: () => "",
|
|
94
|
-
buildCliReferenceSection: () => "",
|
|
95
106
|
ensurePromptFiles: () => {},
|
|
96
107
|
stripCommentLines: (s: string) => s,
|
|
97
108
|
}));
|
|
@@ -165,7 +176,6 @@ mock.module("../../runtime/pre-first-message-gate.js", () => ({
|
|
|
165
176
|
hasReceivedUserMessage: () => preFirstMessageGateOpen,
|
|
166
177
|
}));
|
|
167
178
|
|
|
168
|
-
|
|
169
179
|
const { HeartbeatService } = await import("../heartbeat-service.js");
|
|
170
180
|
|
|
171
181
|
let origWorkspaceDir: string | undefined;
|
|
@@ -178,6 +188,7 @@ beforeEach(() => {
|
|
|
178
188
|
runBackgroundJobCalls.length = 0;
|
|
179
189
|
skipHeartbeatRunCalls.length = 0;
|
|
180
190
|
preFirstMessageGateOpen = true;
|
|
191
|
+
stubConfig.heartbeat.maxConsecutiveRuns = null;
|
|
181
192
|
runBackgroundJobImpl = async () => ({
|
|
182
193
|
conversationId: STUB_CONVERSATION_ID,
|
|
183
194
|
ok: true,
|
|
@@ -351,9 +362,127 @@ describe("HeartbeatService", () => {
|
|
|
351
362
|
|
|
352
363
|
expect(runBackgroundJobCalls).toHaveLength(1);
|
|
353
364
|
expect(
|
|
354
|
-
skipHeartbeatRunCalls.some(
|
|
355
|
-
(c) => c.reason === "pre_first_user_message",
|
|
356
|
-
),
|
|
365
|
+
skipHeartbeatRunCalls.some((c) => c.reason === "pre_first_user_message"),
|
|
357
366
|
).toBe(false);
|
|
358
367
|
});
|
|
368
|
+
|
|
369
|
+
describe("max consecutive runs cap", () => {
|
|
370
|
+
test("skips with reason 'max_consecutive_runs' after the cap is hit", async () => {
|
|
371
|
+
stubConfig.heartbeat.maxConsecutiveRuns = 2;
|
|
372
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
373
|
+
|
|
374
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
375
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
376
|
+
expect(await service.runOnce({ force: false })).toBe(false);
|
|
377
|
+
|
|
378
|
+
expect(runBackgroundJobCalls).toHaveLength(2);
|
|
379
|
+
expect(
|
|
380
|
+
skipHeartbeatRunCalls.some((c) => c.reason === "max_consecutive_runs"),
|
|
381
|
+
).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("resetTimer() clears the counter so auto runs resume", async () => {
|
|
385
|
+
stubConfig.heartbeat.maxConsecutiveRuns = 1;
|
|
386
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
387
|
+
service.start();
|
|
388
|
+
try {
|
|
389
|
+
await service.runOnce({ force: false });
|
|
390
|
+
expect(await service.runOnce({ force: false })).toBe(false);
|
|
391
|
+
|
|
392
|
+
service.resetTimer();
|
|
393
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
394
|
+
expect(runBackgroundJobCalls).toHaveLength(2);
|
|
395
|
+
} finally {
|
|
396
|
+
await service.stop();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("resetTimer() during an in-flight run is not undone by that run's increment", async () => {
|
|
401
|
+
// Regression: if a guardian message arrives mid-run, `resetTimer()`
|
|
402
|
+
// zeroes the counter but the in-flight run's `finally` block used to
|
|
403
|
+
// unconditionally `_consecutiveRuns++`, leaving the counter at 1 and
|
|
404
|
+
// tripping the cap-at-1 path one auto run too early.
|
|
405
|
+
stubConfig.heartbeat.maxConsecutiveRuns = 1;
|
|
406
|
+
|
|
407
|
+
let releaseInflight: () => void = () => {};
|
|
408
|
+
const inflight = new Promise<void>((resolve) => {
|
|
409
|
+
releaseInflight = resolve;
|
|
410
|
+
});
|
|
411
|
+
runBackgroundJobImpl = async () => {
|
|
412
|
+
await inflight;
|
|
413
|
+
return { conversationId: STUB_CONVERSATION_ID, ok: true };
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
417
|
+
service.start();
|
|
418
|
+
try {
|
|
419
|
+
const runPromise = service.runOnce({ force: false });
|
|
420
|
+
// Guardian message arrives while the run is still executing.
|
|
421
|
+
service.resetTimer();
|
|
422
|
+
releaseInflight();
|
|
423
|
+
expect(await runPromise).toBe(true);
|
|
424
|
+
|
|
425
|
+
// The reset during the in-flight run must survive: the next auto run
|
|
426
|
+
// should proceed because the counter is still 0, not 1.
|
|
427
|
+
runBackgroundJobImpl = async () => ({
|
|
428
|
+
conversationId: STUB_CONVERSATION_ID,
|
|
429
|
+
ok: true,
|
|
430
|
+
});
|
|
431
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
432
|
+
expect(
|
|
433
|
+
skipHeartbeatRunCalls.some(
|
|
434
|
+
(c) => c.reason === "max_consecutive_runs",
|
|
435
|
+
),
|
|
436
|
+
).toBe(false);
|
|
437
|
+
} finally {
|
|
438
|
+
await service.stop();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("null disables the cap entirely", async () => {
|
|
443
|
+
stubConfig.heartbeat.maxConsecutiveRuns = null;
|
|
444
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
445
|
+
|
|
446
|
+
for (let i = 0; i < 5; i++) {
|
|
447
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
448
|
+
}
|
|
449
|
+
expect(runBackgroundJobCalls).toHaveLength(5);
|
|
450
|
+
expect(
|
|
451
|
+
skipHeartbeatRunCalls.some((c) => c.reason === "max_consecutive_runs"),
|
|
452
|
+
).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("force runs bypass the cap and do not increment the counter", async () => {
|
|
456
|
+
stubConfig.heartbeat.maxConsecutiveRuns = 2;
|
|
457
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
458
|
+
|
|
459
|
+
// Five force runs would push us well past the cap if force counted.
|
|
460
|
+
for (let i = 0; i < 5; i++) {
|
|
461
|
+
expect(await service.runOnce({ force: true })).toBe(true);
|
|
462
|
+
}
|
|
463
|
+
// Two auto runs should still proceed because the counter is at zero.
|
|
464
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
465
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
466
|
+
// The third auto run trips the cap.
|
|
467
|
+
expect(await service.runOnce({ force: false })).toBe(false);
|
|
468
|
+
expect(
|
|
469
|
+
skipHeartbeatRunCalls.some((c) => c.reason === "max_consecutive_runs"),
|
|
470
|
+
).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("reconfigure() resets the counter", async () => {
|
|
474
|
+
stubConfig.heartbeat.maxConsecutiveRuns = 1;
|
|
475
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
476
|
+
service.start();
|
|
477
|
+
try {
|
|
478
|
+
await service.runOnce({ force: false });
|
|
479
|
+
expect(await service.runOnce({ force: false })).toBe(false);
|
|
480
|
+
|
|
481
|
+
service.reconfigure();
|
|
482
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
483
|
+
} finally {
|
|
484
|
+
await service.stop();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
359
488
|
});
|
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
shouldLogDiskPressureBackgroundSkip,
|
|
10
10
|
} from "../daemon/disk-pressure-background-gate.js";
|
|
11
11
|
import type { HeartbeatAlert } from "../daemon/message-protocol.js";
|
|
12
|
-
import { getConversation, getMessages } from "../memory/conversation-crud.js";
|
|
13
|
-
import { GENERATING_TITLE } from "../memory/conversation-title-service.js";
|
|
14
12
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
15
13
|
import {
|
|
16
14
|
GUARDIAN_PERSONA_TEMPLATE,
|
|
@@ -47,9 +45,6 @@ const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still ac
|
|
|
47
45
|
const EARLY_HEARTBEAT_THRESHOLD = 3;
|
|
48
46
|
const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
|
|
49
47
|
const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
50
|
-
const HEARTBEAT_ALERT_MARKER = "HEARTBEAT_ALERT";
|
|
51
|
-
const HEARTBEAT_OK_MARKER = "HEARTBEAT_OK";
|
|
52
|
-
const HEARTBEAT_ALERT_SUMMARY_MAX_CHARS = 700;
|
|
53
48
|
|
|
54
49
|
// Stripped-comment form of the guardian persona scaffold. Computed
|
|
55
50
|
// once at module load because stripping comment lines is deterministic
|
|
@@ -102,69 +97,6 @@ function recordReengagementTimestamp(): void {
|
|
|
102
97
|
}
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
type HeartbeatDisposition = "alert" | "ok" | "unknown";
|
|
106
|
-
|
|
107
|
-
function parseHeartbeatDisposition(text: string | null): HeartbeatDisposition {
|
|
108
|
-
if (!text) return "unknown";
|
|
109
|
-
const lines = text
|
|
110
|
-
.trim()
|
|
111
|
-
.split(/\r?\n/)
|
|
112
|
-
.map((line) => line.trim())
|
|
113
|
-
.filter((line) => line.length > 0);
|
|
114
|
-
const lastLine = lines.at(-1);
|
|
115
|
-
if (lastLine === HEARTBEAT_ALERT_MARKER) return "alert";
|
|
116
|
-
if (lastLine === HEARTBEAT_OK_MARKER) return "ok";
|
|
117
|
-
return "unknown";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function stripHeartbeatDispositionMarkers(text: string): string {
|
|
121
|
-
return text
|
|
122
|
-
.replace(
|
|
123
|
-
new RegExp(
|
|
124
|
-
`(?:\\r?\\n)?\\s*(?:${HEARTBEAT_ALERT_MARKER}|${HEARTBEAT_OK_MARKER})\\s*$`,
|
|
125
|
-
),
|
|
126
|
-
"",
|
|
127
|
-
)
|
|
128
|
-
.trim();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function truncateSummary(text: string, maxChars: number): string {
|
|
132
|
-
if (text.length <= maxChars) return text;
|
|
133
|
-
return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function buildHeartbeatAlertSummary(text: string | null): string {
|
|
137
|
-
const summary = text ? stripHeartbeatDispositionMarkers(text) : "";
|
|
138
|
-
return truncateSummary(
|
|
139
|
-
summary || "Your assistant found something worth your attention.",
|
|
140
|
-
HEARTBEAT_ALERT_SUMMARY_MAX_CHARS,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function extractVisibleTextFromStoredMessageContent(raw: string): string {
|
|
145
|
-
try {
|
|
146
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
147
|
-
if (typeof parsed === "string") return parsed;
|
|
148
|
-
if (!Array.isArray(parsed)) return "";
|
|
149
|
-
const texts: string[] = [];
|
|
150
|
-
for (const block of parsed) {
|
|
151
|
-
if (
|
|
152
|
-
block != null &&
|
|
153
|
-
typeof block === "object" &&
|
|
154
|
-
"type" in block &&
|
|
155
|
-
block.type === "text" &&
|
|
156
|
-
"text" in block &&
|
|
157
|
-
typeof block.text === "string"
|
|
158
|
-
) {
|
|
159
|
-
texts.push(block.text);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return texts.join("\n").trim();
|
|
163
|
-
} catch {
|
|
164
|
-
return raw;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
100
|
export interface HeartbeatDeps {
|
|
169
101
|
alerter: (alert: HeartbeatAlert) => void;
|
|
170
102
|
onConversationCreated?: (info: {
|
|
@@ -198,6 +130,13 @@ export class HeartbeatService {
|
|
|
198
130
|
private _startupMissedCount = 0;
|
|
199
131
|
private _startupCrashedCount = 0;
|
|
200
132
|
private _hasRunStartupRecovery = false;
|
|
133
|
+
// Counter of consecutive auto-heartbeats since the last guardian message.
|
|
134
|
+
// Reset by resetTimer (guardian message), reconfigure, and stop. Force runs
|
|
135
|
+
// bypass the cap and do not increment.
|
|
136
|
+
private _consecutiveRuns = 0;
|
|
137
|
+
// Bumped every time the counter is reset so an in-flight run that finishes
|
|
138
|
+
// after a guardian message can detect the reset and skip its increment.
|
|
139
|
+
private _resetGeneration = 0;
|
|
201
140
|
|
|
202
141
|
constructor(deps: HeartbeatDeps) {
|
|
203
142
|
this.deps = deps;
|
|
@@ -262,6 +201,11 @@ export class HeartbeatService {
|
|
|
262
201
|
isAsyncBackground: true,
|
|
263
202
|
visibleInSourceNow: false,
|
|
264
203
|
},
|
|
204
|
+
conversationMetadata: {
|
|
205
|
+
source: "heartbeat",
|
|
206
|
+
groupId: "system:background",
|
|
207
|
+
conversationType: "background",
|
|
208
|
+
},
|
|
265
209
|
}).catch((err) => {
|
|
266
210
|
log.warn(
|
|
267
211
|
{ err },
|
|
@@ -350,6 +294,8 @@ export class HeartbeatService {
|
|
|
350
294
|
|
|
351
295
|
/** Restart the timer with the latest config (e.g. after settings change). */
|
|
352
296
|
reconfigure(): void {
|
|
297
|
+
this._consecutiveRuns = 0;
|
|
298
|
+
this._resetGeneration++;
|
|
353
299
|
this.configEpoch++;
|
|
354
300
|
if (this._pendingRunId) {
|
|
355
301
|
supersedePendingRun(this._pendingRunId);
|
|
@@ -371,6 +317,10 @@ export class HeartbeatService {
|
|
|
371
317
|
* after an active conversation.
|
|
372
318
|
*/
|
|
373
319
|
resetTimer(): void {
|
|
320
|
+
// Counter resets even when the timer is null so a guardian message during
|
|
321
|
+
// a stopped window still clears the count.
|
|
322
|
+
this._consecutiveRuns = 0;
|
|
323
|
+
this._resetGeneration++;
|
|
374
324
|
if (!this.timer) return;
|
|
375
325
|
if (this.cronMode) {
|
|
376
326
|
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
@@ -390,6 +340,8 @@ export class HeartbeatService {
|
|
|
390
340
|
}
|
|
391
341
|
|
|
392
342
|
async stop(): Promise<void> {
|
|
343
|
+
this._consecutiveRuns = 0;
|
|
344
|
+
this._resetGeneration++;
|
|
393
345
|
this.stopped = true;
|
|
394
346
|
if (this.timer) {
|
|
395
347
|
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
@@ -500,6 +452,28 @@ export class HeartbeatService {
|
|
|
500
452
|
}
|
|
501
453
|
}
|
|
502
454
|
|
|
455
|
+
// Cap consecutive auto-runs without a guardian message so the assistant
|
|
456
|
+
// stops burning LLM tokens when the user is away. Force runs (manual
|
|
457
|
+
// operator action) bypass the cap and do not increment the counter.
|
|
458
|
+
if (
|
|
459
|
+
!force &&
|
|
460
|
+
config.maxConsecutiveRuns != null &&
|
|
461
|
+
this._consecutiveRuns >= config.maxConsecutiveRuns
|
|
462
|
+
) {
|
|
463
|
+
log.debug(
|
|
464
|
+
{
|
|
465
|
+
consecutiveRuns: this._consecutiveRuns,
|
|
466
|
+
maxConsecutiveRuns: config.maxConsecutiveRuns,
|
|
467
|
+
},
|
|
468
|
+
"Max consecutive runs reached, skipping",
|
|
469
|
+
);
|
|
470
|
+
if (runId) skipHeartbeatRun(runId, "max_consecutive_runs");
|
|
471
|
+
if (!this.cronMode) {
|
|
472
|
+
this.scheduleNextRun(config.intervalMs);
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
503
477
|
// Overlap prevention
|
|
504
478
|
if (this.activeRun) {
|
|
505
479
|
log.debug("Previous heartbeat run still active, skipping");
|
|
@@ -516,6 +490,11 @@ export class HeartbeatService {
|
|
|
516
490
|
}
|
|
517
491
|
const run = this.executeRun(runId, scheduledFor);
|
|
518
492
|
this.activeRun = run;
|
|
493
|
+
// Snapshot the reset generation so we can detect whether a reset (guardian
|
|
494
|
+
// message, reconfigure, stop) happened while this run was in flight. If it
|
|
495
|
+
// did, the counter was already zeroed and we must not undo that reset by
|
|
496
|
+
// incrementing in `finally`.
|
|
497
|
+
const startGeneration = this._resetGeneration;
|
|
519
498
|
try {
|
|
520
499
|
await run;
|
|
521
500
|
} catch (err) {
|
|
@@ -525,6 +504,9 @@ export class HeartbeatService {
|
|
|
525
504
|
this.activeRun = null;
|
|
526
505
|
}
|
|
527
506
|
this._lastRunAt = Date.now();
|
|
507
|
+
if (!force && this._resetGeneration === startGeneration) {
|
|
508
|
+
this._consecutiveRuns++;
|
|
509
|
+
}
|
|
528
510
|
if (!this.cronMode) {
|
|
529
511
|
this.scheduleNextRun(getConfig().heartbeat.intervalMs);
|
|
530
512
|
}
|
|
@@ -648,6 +630,7 @@ export class HeartbeatService {
|
|
|
648
630
|
conversationMetadata: {
|
|
649
631
|
source: "heartbeat",
|
|
650
632
|
groupId: "system:background",
|
|
633
|
+
conversationType: "background",
|
|
651
634
|
},
|
|
652
635
|
});
|
|
653
636
|
} catch (err) {
|
|
@@ -659,65 +642,6 @@ export class HeartbeatService {
|
|
|
659
642
|
}
|
|
660
643
|
}
|
|
661
644
|
|
|
662
|
-
private getLatestAssistantMessage(
|
|
663
|
-
conversationId: string,
|
|
664
|
-
): { id: string; text: string } | null {
|
|
665
|
-
try {
|
|
666
|
-
const messages = getMessages(conversationId);
|
|
667
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
668
|
-
const message = messages[i]!;
|
|
669
|
-
if (message.role !== "assistant") continue;
|
|
670
|
-
return {
|
|
671
|
-
id: message.id,
|
|
672
|
-
text: extractVisibleTextFromStoredMessageContent(message.content),
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
} catch (err) {
|
|
676
|
-
log.warn(
|
|
677
|
-
{ err, conversationId },
|
|
678
|
-
"Failed to read heartbeat assistant message",
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
return null;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
private async emitHeartbeatAlertNotification(params: {
|
|
685
|
-
runId: string;
|
|
686
|
-
conversationId: string;
|
|
687
|
-
messageId?: string;
|
|
688
|
-
conversationTitle: string;
|
|
689
|
-
summary: string;
|
|
690
|
-
}): Promise<void> {
|
|
691
|
-
const { emitNotificationSignal } =
|
|
692
|
-
await import("../notifications/emit-signal.js");
|
|
693
|
-
|
|
694
|
-
await emitNotificationSignal({
|
|
695
|
-
sourceEventName: "heartbeat.alert",
|
|
696
|
-
sourceChannel: "watcher",
|
|
697
|
-
sourceContextId: params.runId,
|
|
698
|
-
dedupeKey: `heartbeat:alert:${params.runId}`,
|
|
699
|
-
attentionHints: {
|
|
700
|
-
requiresAction: true,
|
|
701
|
-
urgency: "medium",
|
|
702
|
-
isAsyncBackground: true,
|
|
703
|
-
visibleInSourceNow: false,
|
|
704
|
-
},
|
|
705
|
-
contextPayload: {
|
|
706
|
-
title: "Heartbeat Alert",
|
|
707
|
-
summary: params.summary,
|
|
708
|
-
conversationTitle: params.conversationTitle,
|
|
709
|
-
conversationId: params.conversationId,
|
|
710
|
-
messageId: params.messageId,
|
|
711
|
-
},
|
|
712
|
-
routingIntent: "single_channel",
|
|
713
|
-
conversationAffinityHint: { vellum: params.conversationId },
|
|
714
|
-
conversationMetadata: {
|
|
715
|
-
source: "heartbeat",
|
|
716
|
-
groupId: "system:background",
|
|
717
|
-
},
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
|
|
721
645
|
private async executeRun(runId: string, scheduledFor: number): Promise<void> {
|
|
722
646
|
log.info("Running heartbeat");
|
|
723
647
|
|
|
@@ -746,9 +670,9 @@ export class HeartbeatService {
|
|
|
746
670
|
// The runner fires `onConversationCreated` synchronously after
|
|
747
671
|
// bootstrap so the macOS sidebar gets the new conversation
|
|
748
672
|
// immediately rather than waiting up to HEARTBEAT_TIMEOUT_MS for
|
|
749
|
-
// the LLM turn to finish.
|
|
750
|
-
//
|
|
751
|
-
//
|
|
673
|
+
// the LLM turn to finish. If the model judges the run worth
|
|
674
|
+
// surfacing to the guardian, it calls the `notifications` skill
|
|
675
|
+
// directly — no in-band marker.
|
|
752
676
|
let conversationId: string | undefined;
|
|
753
677
|
const result = await runBackgroundJob({
|
|
754
678
|
jobName: "heartbeat",
|
|
@@ -780,62 +704,26 @@ export class HeartbeatService {
|
|
|
780
704
|
"Heartbeat completed",
|
|
781
705
|
);
|
|
782
706
|
|
|
783
|
-
// Mark the run record as ok
|
|
784
|
-
// alert the
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
// contents.
|
|
707
|
+
// Mark the run record as ok. The runner owns failure emission via
|
|
708
|
+
// `activity.failed`; any user-facing alert the model decided to
|
|
709
|
+
// raise was emitted in-band via the `notifications` skill during
|
|
710
|
+
// the turn itself.
|
|
788
711
|
const transitioned = completeHeartbeatRun(runId, {
|
|
789
712
|
status: "ok",
|
|
790
713
|
conversationId: result.conversationId,
|
|
791
714
|
});
|
|
792
715
|
|
|
793
|
-
if (transitioned) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
} catch {
|
|
801
|
-
// Best-effort; fall back to generic title.
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const assistantMessage = this.getLatestAssistantMessage(
|
|
805
|
-
result.conversationId,
|
|
806
|
-
);
|
|
807
|
-
const disposition = parseHeartbeatDisposition(
|
|
808
|
-
assistantMessage?.text ?? null,
|
|
809
|
-
);
|
|
810
|
-
if (disposition === "alert") {
|
|
811
|
-
// Conversation was already surfaced via the runner's bootstrap
|
|
812
|
-
// callback above; alert just needs to emit the notification.
|
|
813
|
-
void this.emitHeartbeatAlertNotification({
|
|
716
|
+
if (transitioned && latenessMs > LATE_THRESHOLD_MS) {
|
|
717
|
+
const lateMinutes = Math.round(latenessMs / 60_000);
|
|
718
|
+
log.warn(
|
|
719
|
+
{
|
|
720
|
+
latenessMs,
|
|
721
|
+
lateMinutes,
|
|
722
|
+
scheduledFor,
|
|
814
723
|
runId,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
summary: buildHeartbeatAlertSummary(assistantMessage?.text ?? null),
|
|
819
|
-
}).catch((err) => {
|
|
820
|
-
log.warn(
|
|
821
|
-
{ err, conversationId: result.conversationId },
|
|
822
|
-
"Failed to emit heartbeat alert notification",
|
|
823
|
-
);
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (latenessMs > LATE_THRESHOLD_MS) {
|
|
828
|
-
const lateMinutes = Math.round(latenessMs / 60_000);
|
|
829
|
-
log.warn(
|
|
830
|
-
{
|
|
831
|
-
latenessMs,
|
|
832
|
-
lateMinutes,
|
|
833
|
-
scheduledFor,
|
|
834
|
-
runId,
|
|
835
|
-
},
|
|
836
|
-
"Heartbeat ran late",
|
|
837
|
-
);
|
|
838
|
-
}
|
|
724
|
+
},
|
|
725
|
+
"Heartbeat ran late",
|
|
726
|
+
);
|
|
839
727
|
}
|
|
840
728
|
return;
|
|
841
729
|
}
|
|
@@ -900,18 +788,14 @@ Do NOT attempt to use tools for these providers — they will fail. Skip any che
|
|
|
900
788
|
</credential-status>`;
|
|
901
789
|
}
|
|
902
790
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
After completing your review, end your response with one of:
|
|
908
|
-
- HEARTBEAT_OK — if everything looks good, no action needed
|
|
909
|
-
- HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
|
|
910
|
-
</heartbeat-disposition>`;
|
|
791
|
+
const disposition = getConfig().heartbeat.disposition;
|
|
792
|
+
if (disposition) {
|
|
793
|
+
prompt += `\n\n<heartbeat-disposition>\n${disposition}\n</heartbeat-disposition>`;
|
|
794
|
+
}
|
|
911
795
|
|
|
912
796
|
if (completedRunCount < EARLY_HEARTBEAT_THRESHOLD) {
|
|
913
797
|
prompt += `\n\n<early-heartbeat>
|
|
914
|
-
This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward
|
|
798
|
+
This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward surfacing it via the notifications skill this time. First impressions matter.
|
|
915
799
|
</early-heartbeat>`;
|
|
916
800
|
}
|
|
917
801
|
|
|
@@ -82,6 +82,62 @@ describe("feedItemSchema — valid minimal items", () => {
|
|
|
82
82
|
expect(parsed.expiresAt).toBe("2026-04-15T00:00:00.000Z");
|
|
83
83
|
expect(parsed.detailPanel?.kind).toBe("emailDraft");
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
test("category field passes through when present", () => {
|
|
87
|
+
for (const cat of [
|
|
88
|
+
"security",
|
|
89
|
+
"scheduling",
|
|
90
|
+
"background",
|
|
91
|
+
"email",
|
|
92
|
+
"system",
|
|
93
|
+
] as const) {
|
|
94
|
+
const parsed = feedItemSchema.parse({
|
|
95
|
+
...minimalNotification(),
|
|
96
|
+
category: cat,
|
|
97
|
+
});
|
|
98
|
+
expect(parsed.category).toBe(cat);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("metadata field passes through when present", () => {
|
|
103
|
+
const parsed = feedItemSchema.parse({
|
|
104
|
+
...minimalNotification(),
|
|
105
|
+
metadata: { subject: "Hello", count: 3, nested: { ok: true } },
|
|
106
|
+
});
|
|
107
|
+
expect(parsed.metadata).toEqual({
|
|
108
|
+
subject: "Hello",
|
|
109
|
+
count: 3,
|
|
110
|
+
nested: { ok: true },
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("items without category or metadata still parse (backward compat)", () => {
|
|
115
|
+
const parsed = feedItemSchema.parse(minimalNotification());
|
|
116
|
+
expect(parsed.category).toBeUndefined();
|
|
117
|
+
expect(parsed.metadata).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("noteworthy field passes through when present", () => {
|
|
121
|
+
const parsed = feedItemSchema.parse({
|
|
122
|
+
...minimalNotification(),
|
|
123
|
+
noteworthy: true,
|
|
124
|
+
});
|
|
125
|
+
expect(parsed.noteworthy).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("items without noteworthy field still parse (backward compat)", () => {
|
|
129
|
+
const parsed = feedItemSchema.parse(minimalNotification());
|
|
130
|
+
expect(parsed.noteworthy).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("title is optional and may be omitted", () => {
|
|
134
|
+
const { title: _omitted, ...rest } = minimalNotification();
|
|
135
|
+
const parsed = feedItemSchema.parse(rest);
|
|
136
|
+
expect(parsed.title).toBeUndefined();
|
|
137
|
+
expect(parsed.summary).toBe(
|
|
138
|
+
"You mentioned wanting to review the onboarding designs.",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
85
141
|
});
|
|
86
142
|
|
|
87
143
|
// ---------------------------------------------------------------------------
|
|
@@ -142,6 +198,12 @@ describe("feedItemSchema — enum validation", () => {
|
|
|
142
198
|
}
|
|
143
199
|
});
|
|
144
200
|
|
|
201
|
+
test("rejects unknown `category`", () => {
|
|
202
|
+
expect(() =>
|
|
203
|
+
feedItemSchema.parse({ ...minimalNotification(), category: "weather" }),
|
|
204
|
+
).toThrow();
|
|
205
|
+
});
|
|
206
|
+
|
|
145
207
|
test("rejects unknown `status`", () => {
|
|
146
208
|
expect(() =>
|
|
147
209
|
feedItemSchema.parse({ ...minimalNotification(), status: "archived" }),
|
|
@@ -204,4 +266,22 @@ describe("parseFeedFile", () => {
|
|
|
204
266
|
}),
|
|
205
267
|
).toThrow();
|
|
206
268
|
});
|
|
269
|
+
|
|
270
|
+
test("accepts a file with a noteworthy item", () => {
|
|
271
|
+
const parsed = parseFeedFile({
|
|
272
|
+
version: 2,
|
|
273
|
+
items: [{ ...minimalNotification(), noteworthy: true }],
|
|
274
|
+
updatedAt: NOW_ISO,
|
|
275
|
+
});
|
|
276
|
+
expect(parsed.items[0]?.noteworthy).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("accepts a file whose items omit noteworthy (backward compat)", () => {
|
|
280
|
+
const parsed = parseFeedFile({
|
|
281
|
+
version: 2,
|
|
282
|
+
items: [minimalNotification()],
|
|
283
|
+
updatedAt: NOW_ISO,
|
|
284
|
+
});
|
|
285
|
+
expect(parsed.items[0]?.noteworthy).toBeUndefined();
|
|
286
|
+
});
|
|
207
287
|
});
|