@vellumai/assistant 0.12.1 → 0.12.2-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +20 -21
- package/docs/architecture/turn-actor.md +15 -8
- package/docs/guardian-request-flow.md +16 -0
- package/openapi.yaml +327 -52
- package/package.json +3 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/anthropic-provider.test.ts +0 -63
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
- package/src/__tests__/assistant-event-hub.test.ts +44 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +19 -7
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +212 -3
- package/src/__tests__/credential-security-invariants.test.ts +4 -2
- package/src/__tests__/cu-unified-flow.test.ts +8 -2
- package/src/__tests__/document-sync-tags.test.ts +25 -0
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/heartbeat-service.test.ts +56 -40
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-cu-proxy.test.ts +539 -0
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
- package/src/__tests__/oauth-commands-routes.test.ts +113 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
- package/src/__tests__/openai-provider.test.ts +1 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-callback-registration.test.ts +8 -16
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
- package/src/__tests__/qdrant-manager.test.ts +132 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/service-account-token.test.ts +247 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/unicode.test.ts +13 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +96 -10
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/agent/loop.ts +1 -0
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/events/notification-intent.ts +2 -0
- package/src/api/events/user-message-echo.ts +7 -1
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/avatar/identity-avatar.ts +7 -2
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +53 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +59 -46
- package/src/calls/voice-triage-escalate.ts +175 -56
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/bash.test.ts +53 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/bash.help.ts +8 -13
- package/src/cli/commands/bash.ts +7 -25
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +255 -0
- package/src/config/__tests__/platform-rehydration.test.ts +99 -9
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
- package/src/config/bundled-skills/schedule/SKILL.md +3 -4
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +24 -12
- package/src/config/loader.ts +1 -0
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +201 -0
- package/src/config/platform-rehydration.ts +11 -27
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/services.ts +8 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/credential-health/credential-health-service.ts +11 -17
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-error.ts +3 -2
- package/src/daemon/conversation-media-retry.ts +2 -1
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-runtime-assembly.ts +2 -1
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +15 -9
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/dictation-profile-store.ts +2 -1
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +6 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-cu-proxy.ts +186 -19
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-proxy-preactivation.ts +2 -1
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/orphan-reaper.ts +1 -1
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/reaction-history-render.ts +2 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +122 -0
- package/src/desktop/desktop-chrome-policy.test.ts +79 -0
- package/src/desktop/desktop-chrome-policy.ts +47 -0
- package/src/desktop/desktop-chrome-session.ts +37 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +308 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +730 -0
- package/src/desktop/desktop-session-manager.ts +850 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-drag.ts +177 -0
- package/src/desktop/desktop-window-manager-config.test.ts +230 -0
- package/src/desktop/desktop-window-manager-config.ts +238 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/heartbeat/heartbeat-service.ts +21 -3
- package/src/home/__tests__/feed-writer.test.ts +22 -0
- package/src/home/feed-writer.ts +11 -2
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/live-voice-photo.ts +25 -6
- package/src/live-voice/live-voice-session.ts +213 -693
- package/src/live-voice/protocol.ts +0 -2
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/guardian-feed-projection.ts +125 -5
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/notifications/schedule-result-producer.ts +66 -22
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
- package/src/oauth/byo-connection.test.ts +105 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +73 -0
- package/src/oauth/connection-resolver.ts +95 -2
- package/src/oauth/scope-utils.ts +24 -0
- package/src/oauth/seed-providers.ts +204 -25
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-queries.ts +2 -2
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/embeddings/qdrant-manager.ts +163 -18
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +4 -2
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
- package/src/plugins/defaults/memory/context-search/format.ts +3 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +28 -2
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
- package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
- package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
- package/src/plugins/defaults/memory/v3/card.ts +3 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
- package/src/plugins/defaults/memory/v3/sections.ts +9 -3
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/__tests__/retry-callsite.test.ts +41 -0
- package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
- package/src/providers/anthropic/client.ts +0 -47
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
- package/src/providers/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/endpoint-probe.ts +10 -1
- package/src/providers/inference/resolve-auth.ts +22 -4
- package/src/providers/inference/service-account-token.ts +248 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +212 -79
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/providers/opencode/client.test.ts +73 -4
- package/src/providers/opencode/client.ts +60 -3
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/outbound-request-sanitize.ts +96 -0
- package/src/providers/retry.ts +30 -19
- package/src/providers/unparseable-tool-args.ts +5 -1
- package/src/runtime/AGENTS.md +3 -1
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/host-observe.test.ts +40 -1
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/agent-wake.ts +2 -1
- package/src/runtime/assistant-event-hub.ts +21 -3
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/host-observe.ts +11 -1
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +73 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/conversation-routes.ts +16 -3
- package/src/runtime/routes/credential-routes.ts +19 -10
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +61 -4
- package/src/runtime/routes/events-routes.ts +4 -0
- package/src/runtime/routes/host-cu-routes.ts +6 -0
- package/src/runtime/routes/index.ts +4 -2
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +6 -20
- package/src/runtime/routes/oauth-commands-routes.ts +36 -0
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/routes/workspace-routes.test.ts +55 -26
- package/src/runtime/routes/workspace-routes.ts +46 -63
- package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
- package/src/runtime/routes/workspace-tree-walk.ts +240 -0
- package/src/runtime/routes/workspace-utils.ts +17 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/run-script.ts +2 -1
- package/src/schedule/schedule-store.ts +9 -2
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/security/untrusted-content.ts +3 -1
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
- package/src/tools/browser/network-recorder.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +121 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/filesystem/search.ts +2 -1
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/network/web-search.ts +6 -1
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/skills/scaffold-managed.ts +61 -3
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/types/host-capabilities.ts +5 -3
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/util/unicode.ts +15 -1
- package/src/watch/watch-timeline.ts +2 -2
- package/src/workflows/run-manager.ts +2 -1
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +6 -55
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/runtime/routes/debug-bash-routes.ts +0 -173
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
6
6
|
|
|
7
7
|
import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
|
|
8
|
+
import { MESSAGE_KEYS } from "../../i18n/index.js";
|
|
8
9
|
import type { AttentionState } from "../../persistence/conversation-attention-store.js";
|
|
9
10
|
import type {
|
|
10
11
|
ConversationRow,
|
|
@@ -589,6 +590,60 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
589
590
|
});
|
|
590
591
|
});
|
|
591
592
|
|
|
593
|
+
// A conversation whose title is still being written stores the message key
|
|
594
|
+
// itself, which is a non-empty string the sanitizer has no reason to reject.
|
|
595
|
+
// Sending it would put a raw key on the lock screen, so it counts as absent.
|
|
596
|
+
test("omits requestedTitle while the title is still generating", async () => {
|
|
597
|
+
conversationRow = makeConversation({
|
|
598
|
+
title: MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
await run();
|
|
602
|
+
|
|
603
|
+
expect(emitCalls).toHaveLength(1);
|
|
604
|
+
expect(emitCalls[0].contextPayload).toEqual({
|
|
605
|
+
requestedMessage: "Sure, here is the plan.",
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("omits requestedTitle for the legacy generating placeholder", async () => {
|
|
610
|
+
conversationRow = makeConversation({ title: "Generating title..." });
|
|
611
|
+
|
|
612
|
+
await run();
|
|
613
|
+
|
|
614
|
+
expect(emitCalls).toHaveLength(1);
|
|
615
|
+
expect(emitCalls[0].contextPayload).toEqual({
|
|
616
|
+
requestedMessage: "Sure, here is the plan.",
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("omits requestedTitle for the untitled placeholder key", async () => {
|
|
621
|
+
conversationRow = makeConversation({
|
|
622
|
+
title: MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
await run();
|
|
626
|
+
|
|
627
|
+
expect(emitCalls).toHaveLength(1);
|
|
628
|
+
expect(emitCalls[0].contextPayload).toEqual({
|
|
629
|
+
requestedMessage: "Sure, here is the plan.",
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Only an exact key is a system constant. A title the user or the model
|
|
634
|
+
// wrote that happens to contain one is their copy and travels as written.
|
|
635
|
+
test("keeps a title that merely contains a message key", async () => {
|
|
636
|
+
conversationRow = makeConversation({
|
|
637
|
+
title: `notes on ${MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING}`,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await run();
|
|
641
|
+
|
|
642
|
+
expect(emitCalls[0].contextPayload.requestedTitle).toBe(
|
|
643
|
+
"notes on conversation.title.generating",
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
592
647
|
test("caps the preview at 200 chars", async () => {
|
|
593
648
|
assistantRow = makeAssistantRow([
|
|
594
649
|
{ type: "text", text: "a".repeat(300) },
|
|
@@ -39,15 +39,35 @@ mock.module("../../channels/gateway-guardian-requests.js", () => ({
|
|
|
39
39
|
getGuardianRequest: (id: string) => getGuardianRequestStub(id),
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
|
+
// The heal's completion sentinel lives in the daemon checkpoint ledger. An
|
|
43
|
+
// in-memory map stands in so the heal runs without a database; it is cleared
|
|
44
|
+
// per test so every test starts unhealed. The spread keeps the module's other
|
|
45
|
+
// exports real for transitive importers.
|
|
46
|
+
const actualCheckpoints = await import("../../persistence/checkpoints.js");
|
|
47
|
+
const checkpointStore = new Map<string, string>();
|
|
48
|
+
mock.module("../../persistence/checkpoints.js", () => ({
|
|
49
|
+
...actualCheckpoints,
|
|
50
|
+
getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
|
|
51
|
+
setMemoryCheckpoint: (key: string, value: string) => {
|
|
52
|
+
checkpointStore.set(key, value);
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
|
|
42
56
|
const {
|
|
43
57
|
buildPendingGuardianProjection,
|
|
44
58
|
guardianFeedItemId,
|
|
59
|
+
healLegacyGuardianReceiptUnread,
|
|
45
60
|
reconcileGuardianFeedProjections,
|
|
46
61
|
requestIdFromGuardianFeedItemId,
|
|
47
62
|
writeGuardianFeedReceipt,
|
|
48
63
|
} = await import("../guardian-feed-projection.js");
|
|
49
|
-
const {
|
|
50
|
-
|
|
64
|
+
const {
|
|
65
|
+
appendFeedItem,
|
|
66
|
+
bulkSetFeedItemStatus,
|
|
67
|
+
getHomeFeedPath,
|
|
68
|
+
patchFeedItemStatus,
|
|
69
|
+
readHomeFeed,
|
|
70
|
+
} = await import("../../home/feed-writer.js");
|
|
51
71
|
const { GUARDIAN_TERMINAL_REASON_SUPERSEDED, isPendingGuardianFeedItem } =
|
|
52
72
|
await import("../../api/responses/home.js");
|
|
53
73
|
type FeedItem = import("../../api/responses/home.js").FeedItem;
|
|
@@ -61,6 +81,7 @@ beforeEach(() => {
|
|
|
61
81
|
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
62
82
|
listGuardianRequestsStub = async () => [];
|
|
63
83
|
getGuardianRequestStub = async () => null;
|
|
84
|
+
checkpointStore.clear();
|
|
64
85
|
});
|
|
65
86
|
|
|
66
87
|
afterEach(() => {
|
|
@@ -104,6 +125,72 @@ function pendingGuardianItem(requestId: string): FeedItem {
|
|
|
104
125
|
};
|
|
105
126
|
}
|
|
106
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A terminal receipt that never passed the pending-to-terminal edge:
|
|
130
|
+
* terminal projection, urgency `medium`, still `new`.
|
|
131
|
+
*/
|
|
132
|
+
function legacyGuardianReceiptItem(requestId: string): FeedItem {
|
|
133
|
+
const item = pendingGuardianItem(requestId);
|
|
134
|
+
return {
|
|
135
|
+
...item,
|
|
136
|
+
urgency: "medium",
|
|
137
|
+
status: "new",
|
|
138
|
+
guardianRequest: { ...item.guardianRequest!, status: "approved" },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
describe("healLegacyGuardianReceiptUnread", () => {
|
|
143
|
+
test("clears unread on a terminal receipt that never passed the edge", async () => {
|
|
144
|
+
await appendFeedItem(legacyGuardianReceiptItem("legacy-1"));
|
|
145
|
+
|
|
146
|
+
await healLegacyGuardianReceiptUnread();
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
readHomeFeed().items.find((i) => i.id === guardianFeedItemId("legacy-1"))
|
|
150
|
+
?.status,
|
|
151
|
+
).toBe("seen");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("runs once per assistant, so a later mark-unread stands", async () => {
|
|
155
|
+
await appendFeedItem(legacyGuardianReceiptItem("legacy-2"));
|
|
156
|
+
const itemId = guardianFeedItemId("legacy-2");
|
|
157
|
+
await healLegacyGuardianReceiptUnread();
|
|
158
|
+
|
|
159
|
+
// The user marks the resolved receipt unread again. The pass has
|
|
160
|
+
// already recorded completion, so a second round leaves it alone.
|
|
161
|
+
await patchFeedItemStatus(itemId, "new");
|
|
162
|
+
await healLegacyGuardianReceiptUnread();
|
|
163
|
+
|
|
164
|
+
expect(readHomeFeed().items.find((i) => i.id === itemId)?.status).toBe(
|
|
165
|
+
"new",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("never revives a dismissed receipt", async () => {
|
|
170
|
+
const item = legacyGuardianReceiptItem("legacy-3");
|
|
171
|
+
await appendFeedItem({ ...item, status: "dismissed" });
|
|
172
|
+
|
|
173
|
+
await healLegacyGuardianReceiptUnread();
|
|
174
|
+
|
|
175
|
+
expect(
|
|
176
|
+
readHomeFeed().items.find((i) => i.id === guardianFeedItemId("legacy-3"))
|
|
177
|
+
?.status,
|
|
178
|
+
).toBe("dismissed");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("leaves a still-pending request alone", async () => {
|
|
182
|
+
await appendFeedItem(pendingGuardianItem("still-pending"));
|
|
183
|
+
|
|
184
|
+
await healLegacyGuardianReceiptUnread();
|
|
185
|
+
|
|
186
|
+
const item = readHomeFeed().items.find(
|
|
187
|
+
(i) => i.id === guardianFeedItemId("still-pending"),
|
|
188
|
+
);
|
|
189
|
+
expect(item?.status).toBe("new");
|
|
190
|
+
expect(item && isPendingGuardianFeedItem(item)).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
107
194
|
describe("buildPendingGuardianProjection", () => {
|
|
108
195
|
test("tool approval projects as a pending approval with source facts", () => {
|
|
109
196
|
const projection = buildPendingGuardianProjection(toolApprovalPayload);
|
|
@@ -264,6 +351,75 @@ describe("writeGuardianFeedReceipt", () => {
|
|
|
264
351
|
expect(item?.guardianRequest?.status).toBe("denied");
|
|
265
352
|
});
|
|
266
353
|
|
|
354
|
+
test("a receipt stops the item reading as unread", async () => {
|
|
355
|
+
await appendFeedItem(pendingGuardianItem("req-6"));
|
|
356
|
+
await writeGuardianFeedReceipt({
|
|
357
|
+
requestId: "req-6",
|
|
358
|
+
status: "approved",
|
|
359
|
+
decidedAction: "approve_once",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const item = readHomeFeed().items.find(
|
|
363
|
+
(i) => i.id === guardianFeedItemId("req-6"),
|
|
364
|
+
);
|
|
365
|
+
// The clients' unread test is `status === "new"`, so a receipt left
|
|
366
|
+
// at `new` keeps lighting the bell for work nobody can act on.
|
|
367
|
+
expect(item?.status).toBe("seen");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("a terminal status nobody decided also clears unread", async () => {
|
|
371
|
+
await appendFeedItem(pendingGuardianItem("req-7"));
|
|
372
|
+
await writeGuardianFeedReceipt({ requestId: "req-7", status: "expired" });
|
|
373
|
+
|
|
374
|
+
expect(
|
|
375
|
+
readHomeFeed().items.find((i) => i.id === guardianFeedItemId("req-7"))
|
|
376
|
+
?.status,
|
|
377
|
+
).toBe("seen");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("a status the user set survives a re-run of the fan-out", async () => {
|
|
381
|
+
await appendFeedItem(pendingGuardianItem("req-8"));
|
|
382
|
+
const itemId = guardianFeedItemId("req-8");
|
|
383
|
+
await writeGuardianFeedReceipt({ requestId: "req-8", status: "approved" });
|
|
384
|
+
// The user clears the receipt, then the withdrawal fan-out retries
|
|
385
|
+
// (its per-request receipt is held back until every surface settles).
|
|
386
|
+
await patchFeedItemStatus(itemId, "dismissed");
|
|
387
|
+
await writeGuardianFeedReceipt({ requestId: "req-8", status: "approved" });
|
|
388
|
+
|
|
389
|
+
expect(readHomeFeed().items.find((i) => i.id === itemId)?.status).toBe(
|
|
390
|
+
"dismissed",
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("a pending request dismissed outright is not revived by its receipt", async () => {
|
|
395
|
+
await appendFeedItem(pendingGuardianItem("req-10"));
|
|
396
|
+
const itemId = guardianFeedItemId("req-10");
|
|
397
|
+
// Bulk dismissal spares a pending guardian item, but a deliberate
|
|
398
|
+
// single-item dismissal stays available and is not undone by the
|
|
399
|
+
// resolution that follows it.
|
|
400
|
+
await patchFeedItemStatus(itemId, "dismissed");
|
|
401
|
+
await writeGuardianFeedReceipt({ requestId: "req-10", status: "denied" });
|
|
402
|
+
|
|
403
|
+
expect(readHomeFeed().items.find((i) => i.id === itemId)?.status).toBe(
|
|
404
|
+
"dismissed",
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("a receipt the user marked unread again stays unread", async () => {
|
|
409
|
+
await appendFeedItem(pendingGuardianItem("req-9"));
|
|
410
|
+
const itemId = guardianFeedItemId("req-9");
|
|
411
|
+
await writeGuardianFeedReceipt({ requestId: "req-9", status: "approved" });
|
|
412
|
+
// The bell detail's read toggle writes `new` back onto a resolved
|
|
413
|
+
// receipt. The clear runs on the edge into terminal, which has
|
|
414
|
+
// already passed, so a later receipt must not take it back.
|
|
415
|
+
await patchFeedItemStatus(itemId, "new");
|
|
416
|
+
await writeGuardianFeedReceipt({ requestId: "req-9", status: "approved" });
|
|
417
|
+
|
|
418
|
+
expect(readHomeFeed().items.find((i) => i.id === itemId)?.status).toBe(
|
|
419
|
+
"new",
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
267
423
|
test("a request with no item resolves true (nothing to retry)", async () => {
|
|
268
424
|
expect(
|
|
269
425
|
await writeGuardianFeedReceipt({ requestId: "ghost", status: "expired" }),
|
|
@@ -26,6 +26,15 @@ const messageRewrites: Array<{ messageId: string; content: string }> = [];
|
|
|
26
26
|
/** messageId -> the conversation it belongs to, for the scoped lookup. */
|
|
27
27
|
const messageOwners = new Map<string, string>();
|
|
28
28
|
let conversationRow: { conversationType: string } | null = null;
|
|
29
|
+
/**
|
|
30
|
+
* Rows for specific ids, consulted before the catch-all `conversationRow`,
|
|
31
|
+
* so a test can hand the signal's source and the delivery's paired
|
|
32
|
+
* conversation different shapes.
|
|
33
|
+
*/
|
|
34
|
+
const conversationRowsById = new Map<
|
|
35
|
+
string,
|
|
36
|
+
{ conversationType: string; source?: string }
|
|
37
|
+
>();
|
|
29
38
|
let conversationLookupShouldThrow = false;
|
|
30
39
|
let messageAppendShouldThrow = false;
|
|
31
40
|
let messageRewriteShouldThrow = false;
|
|
@@ -71,7 +80,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
71
80
|
if (conversationLookupShouldThrow) {
|
|
72
81
|
throw new Error("simulated conversation lookup failure");
|
|
73
82
|
}
|
|
74
|
-
return conversationRow;
|
|
83
|
+
return conversationRowsById.get(id) ?? conversationRow;
|
|
75
84
|
},
|
|
76
85
|
addMessage: async (
|
|
77
86
|
conversationId: string,
|
|
@@ -164,6 +173,7 @@ beforeEach(() => {
|
|
|
164
173
|
messageRewrites.length = 0;
|
|
165
174
|
messageOwners.clear();
|
|
166
175
|
conversationRow = null;
|
|
176
|
+
conversationRowsById.clear();
|
|
167
177
|
conversationLookupShouldThrow = false;
|
|
168
178
|
messageAppendShouldThrow = false;
|
|
169
179
|
messageRewriteShouldThrow = false;
|
|
@@ -258,7 +268,11 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
258
268
|
},
|
|
259
269
|
});
|
|
260
270
|
|
|
261
|
-
const item = await writeHomeFeedItemForSignal(
|
|
271
|
+
const item = await writeHomeFeedItemForSignal(
|
|
272
|
+
signal,
|
|
273
|
+
decision,
|
|
274
|
+
makeVellumDelivery(),
|
|
275
|
+
);
|
|
262
276
|
|
|
263
277
|
expect(item).not.toBeNull();
|
|
264
278
|
expect(appendCalls).toHaveLength(1);
|
|
@@ -267,7 +281,10 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
267
281
|
"The deployment completed successfully.",
|
|
268
282
|
);
|
|
269
283
|
expect(appendCalls[0]!.conversationId).toBe("conv-source-1");
|
|
270
|
-
expect(
|
|
284
|
+
expect(appendCalls[0]!.metadata?.notificationConversationMessageId).toBeUndefined();
|
|
285
|
+
expect(messageAppends).toEqual([]);
|
|
286
|
+
expect(messagesInvalidated).toEqual([]);
|
|
287
|
+
expect(conversationLookups).toEqual(["conv-source-1", "conv-source-1"]);
|
|
271
288
|
});
|
|
272
289
|
|
|
273
290
|
test("isAsyncBackground hint writes even when sourceContextId does not resolve", async () => {
|
|
@@ -402,6 +419,139 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
402
419
|
expect(appendCalls[0]!.conversationId).toBe("paired-delivery-conv-id");
|
|
403
420
|
});
|
|
404
421
|
|
|
422
|
+
describe("assistant-initiated thread deliveries", () => {
|
|
423
|
+
// Under the assistant-initiated-threads flag, conversation pairing
|
|
424
|
+
// promotes a background assistant.share into a standard conversation
|
|
425
|
+
// stamped `assistant_initiated`, which the sidebar's assistant section
|
|
426
|
+
// renders. That thread is the share's surface; a feed item for the
|
|
427
|
+
// same signal put it in the bell a second time.
|
|
428
|
+
const assistantThreadDelivery = () =>
|
|
429
|
+
makeVellumDelivery({ conversationId: "assistant-thread-1" });
|
|
430
|
+
|
|
431
|
+
function stampAssistantThread(): void {
|
|
432
|
+
conversationRowsById.set("assistant-thread-1", {
|
|
433
|
+
conversationType: "standard",
|
|
434
|
+
source: "assistant_initiated",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function shareFromHeartbeat(): NotificationSignal {
|
|
439
|
+
return makeSignal({
|
|
440
|
+
sourceChannel: "assistant_tool",
|
|
441
|
+
sourceEventName: "assistant.share",
|
|
442
|
+
sourceContextId: "conv-source-1",
|
|
443
|
+
contextPayload: { title: "Doctor checklist built" },
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const shareDecision = () =>
|
|
448
|
+
makeDecision({
|
|
449
|
+
selectedChannels: ["vellum"],
|
|
450
|
+
renderedCopy: {
|
|
451
|
+
vellum: {
|
|
452
|
+
title: "Doctor checklist built",
|
|
453
|
+
body: "Your doctor-visit one-pager is ready.",
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("a background share promoted into an assistant thread does not mirror", async () => {
|
|
459
|
+
conversationRow = { conversationType: "background" };
|
|
460
|
+
stampAssistantThread();
|
|
461
|
+
|
|
462
|
+
const item = await writeHomeFeedItemForSignal(
|
|
463
|
+
shareFromHeartbeat(),
|
|
464
|
+
shareDecision(),
|
|
465
|
+
assistantThreadDelivery(),
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(item).toBeNull();
|
|
469
|
+
expect(appendCalls).toHaveLength(0);
|
|
470
|
+
expect(messageAppends).toHaveLength(0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("the async-background hint does not override the assistant-thread suppression", async () => {
|
|
474
|
+
conversationRow = null;
|
|
475
|
+
stampAssistantThread();
|
|
476
|
+
const signal = makeSignal({
|
|
477
|
+
sourceChannel: "assistant_tool",
|
|
478
|
+
sourceEventName: "assistant.share",
|
|
479
|
+
sourceContextId: "heartbeat",
|
|
480
|
+
contextPayload: { title: "Bilbao solo trip is planned" },
|
|
481
|
+
attentionHints: {
|
|
482
|
+
requiresAction: false,
|
|
483
|
+
urgency: "medium",
|
|
484
|
+
isAsyncBackground: true,
|
|
485
|
+
visibleInSourceNow: false,
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const item = await writeHomeFeedItemForSignal(
|
|
490
|
+
signal,
|
|
491
|
+
shareDecision(),
|
|
492
|
+
assistantThreadDelivery(),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(item).toBeNull();
|
|
496
|
+
expect(appendCalls).toHaveLength(0);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("a share paired with an ordinary notification thread still mirrors", async () => {
|
|
500
|
+
// Flag off, or any producer that did not promote: pairing writes a
|
|
501
|
+
// `notification`-sourced row, which stays in Chats and the bell.
|
|
502
|
+
conversationRow = { conversationType: "background" };
|
|
503
|
+
conversationRowsById.set("assistant-thread-1", {
|
|
504
|
+
conversationType: "standard",
|
|
505
|
+
source: "notification",
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const item = await writeHomeFeedItemForSignal(
|
|
509
|
+
shareFromHeartbeat(),
|
|
510
|
+
shareDecision(),
|
|
511
|
+
assistantThreadDelivery(),
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
expect(item).not.toBeNull();
|
|
515
|
+
expect(appendCalls).toHaveLength(1);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("a guardian request keeps its bell item even when paired with an assistant thread", async () => {
|
|
519
|
+
// The bell is the request's canonical home: nothing else renders the
|
|
520
|
+
// pending card, so the assistant-thread suppression must not reach it.
|
|
521
|
+
conversationRow = { conversationType: "standard" };
|
|
522
|
+
stampAssistantThread();
|
|
523
|
+
const signal = makeSignal({
|
|
524
|
+
sourceChannel: "assistant_tool",
|
|
525
|
+
sourceEventName: "guardian.question",
|
|
526
|
+
contextPayload: {
|
|
527
|
+
title: "Approve this?",
|
|
528
|
+
questionText: "May I send the email?",
|
|
529
|
+
requestId: "req-1",
|
|
530
|
+
},
|
|
531
|
+
attentionHints: {
|
|
532
|
+
requiresAction: true,
|
|
533
|
+
urgency: "medium",
|
|
534
|
+
isAsyncBackground: false,
|
|
535
|
+
visibleInSourceNow: false,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const item = await writeHomeFeedItemForSignal(
|
|
540
|
+
signal,
|
|
541
|
+
makeDecision({
|
|
542
|
+
selectedChannels: ["vellum"],
|
|
543
|
+
renderedCopy: {
|
|
544
|
+
vellum: { title: "Approve this?", body: "May I send the email?" },
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
assistantThreadDelivery(),
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
expect(item).not.toBeNull();
|
|
551
|
+
expect(appendCalls).toHaveLength(1);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
405
555
|
test("source conversation id wins over the paired delivery fallback when both are available", async () => {
|
|
406
556
|
// When the producer's `sourceContextId` already points at a real
|
|
407
557
|
// conversation (the canonical "where the work happened"), prefer it
|
|
@@ -55,6 +55,13 @@ const SCHEDULE_ID = "sched-1";
|
|
|
55
55
|
const RUN_ID = "run-1";
|
|
56
56
|
const RUN_STARTED_AT = 1700000000000;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The synthetic user rows carrying each tool call's result. A delivery counts
|
|
60
|
+
* only when its call reported success, so a case that wants suppression has to
|
|
61
|
+
* supply the matching successful result.
|
|
62
|
+
*/
|
|
63
|
+
let toolResultRows: MessageRow[] = [];
|
|
64
|
+
|
|
58
65
|
const realCrud = await import("../../persistence/conversation-crud.js");
|
|
59
66
|
mock.module("../../persistence/conversation-crud.js", () => ({
|
|
60
67
|
...realCrud,
|
|
@@ -66,6 +73,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
66
73
|
messageId === ASSISTANT_MESSAGE_ID
|
|
67
74
|
? [...turnRows.map((row) => row.id), ASSISTANT_MESSAGE_ID]
|
|
68
75
|
: [messageId],
|
|
76
|
+
getMessagesAfter: () => toolResultRows,
|
|
69
77
|
}));
|
|
70
78
|
|
|
71
79
|
const realAttentionStore =
|
|
@@ -114,6 +122,25 @@ function makeToolCallRow(
|
|
|
114
122
|
);
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/** The executor's verdict on the call `makeToolCallRow(name, ...)` made. */
|
|
126
|
+
function makeToolResultRow(name: string, isError = false): MessageRow {
|
|
127
|
+
return makeAssistantRow(
|
|
128
|
+
[
|
|
129
|
+
{
|
|
130
|
+
type: "tool_result",
|
|
131
|
+
tool_use_id: `tu-${name}`,
|
|
132
|
+
content: isError ? "failed" : "ok",
|
|
133
|
+
...(isError ? { is_error: true } : {}),
|
|
134
|
+
},
|
|
135
|
+
] as ContentBlock[],
|
|
136
|
+
{
|
|
137
|
+
id: `res-${name}`,
|
|
138
|
+
role: "user",
|
|
139
|
+
createdAt: RUN_STARTED_AT + 150,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
function makeAttentionState(
|
|
118
145
|
overrides: Partial<AttentionState> = {},
|
|
119
146
|
): AttentionState {
|
|
@@ -189,6 +216,7 @@ beforeEach(() => {
|
|
|
189
216
|
notifiedProbeArgs.length = 0;
|
|
190
217
|
alreadyNotified = false;
|
|
191
218
|
turnRows = [];
|
|
219
|
+
toolResultRows = [];
|
|
192
220
|
attentionState = makeAttentionState();
|
|
193
221
|
assistantRow = makeAssistantRow([
|
|
194
222
|
{ type: "text", text: "**3 new emails** and one calendar change." },
|
|
@@ -245,7 +273,8 @@ describe("emitScheduleResultNotification", () => {
|
|
|
245
273
|
|
|
246
274
|
test("stays silent when the run delivered by email through messaging_send", async () => {
|
|
247
275
|
// The schedule skill's prescribed route for rich content. It writes no
|
|
248
|
-
// notification event, so
|
|
276
|
+
// notification event, so the successful tool call is what shows the user
|
|
277
|
+
// has the result.
|
|
249
278
|
turnRows = [
|
|
250
279
|
makeToolCallRow("messaging_send", {
|
|
251
280
|
platform: "gmail",
|
|
@@ -253,6 +282,7 @@ describe("emitScheduleResultNotification", () => {
|
|
|
253
282
|
message: "Inbox digest…",
|
|
254
283
|
}),
|
|
255
284
|
];
|
|
285
|
+
toolResultRows = [makeToolResultRow("messaging_send")];
|
|
256
286
|
assistantRow = makeAssistantRow([
|
|
257
287
|
{ type: "text", text: "Sent the digest to your inbox." },
|
|
258
288
|
] as ContentBlock[]);
|
|
@@ -262,20 +292,86 @@ describe("emitScheduleResultNotification", () => {
|
|
|
262
292
|
expect(emitCalls).toHaveLength(0);
|
|
263
293
|
});
|
|
264
294
|
|
|
265
|
-
test("stays silent when the run posted to
|
|
295
|
+
test("stays silent when the run posted to a channel through messaging_send", async () => {
|
|
296
|
+
turnRows = [
|
|
297
|
+
makeToolCallRow("messaging_send", {
|
|
298
|
+
platform: "slack",
|
|
299
|
+
conversation_id: "C0123456789",
|
|
300
|
+
text: "the digest",
|
|
301
|
+
}),
|
|
302
|
+
];
|
|
303
|
+
toolResultRows = [makeToolResultRow("messaging_send")];
|
|
304
|
+
assistantRow = makeAssistantRow([
|
|
305
|
+
{ type: "text", text: "Posted the digest to #general." },
|
|
306
|
+
] as ContentBlock[]);
|
|
307
|
+
|
|
308
|
+
await run();
|
|
309
|
+
|
|
310
|
+
expect(emitCalls).toHaveLength(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("notifies when the channel send failed, so the run is not silent", async () => {
|
|
314
|
+
// The failure this guard exists for: the post never landed, the run said
|
|
315
|
+
// so in a conversation nobody has open, and suppressing on the attempt
|
|
316
|
+
// would cost the user the digest and the explanation both.
|
|
317
|
+
turnRows = [
|
|
318
|
+
makeToolCallRow("messaging_send", {
|
|
319
|
+
platform: "slack",
|
|
320
|
+
conversation_id: "C0123456789",
|
|
321
|
+
text: "the digest",
|
|
322
|
+
}),
|
|
323
|
+
];
|
|
324
|
+
toolResultRows = [makeToolResultRow("messaging_send", true)];
|
|
325
|
+
assistantRow = makeAssistantRow([
|
|
326
|
+
{ type: "text", text: "I could not post the digest to #general." },
|
|
327
|
+
] as ContentBlock[]);
|
|
328
|
+
|
|
329
|
+
await run();
|
|
330
|
+
|
|
331
|
+
expect(emitCalls).toHaveLength(1);
|
|
332
|
+
expect(emitCalls[0].contextPayload.requestedMessage).toBe(
|
|
333
|
+
"I could not post the digest to #general.",
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("notifies when a delivery call left no result at all", async () => {
|
|
338
|
+
// An interrupted turn persists the call without its verdict. Unproven is
|
|
339
|
+
// not delivered.
|
|
340
|
+
turnRows = [
|
|
341
|
+
makeToolCallRow("messaging_send", {
|
|
342
|
+
platform: "slack",
|
|
343
|
+
conversation_id: "C0123456789",
|
|
344
|
+
text: "the digest",
|
|
345
|
+
}),
|
|
346
|
+
];
|
|
347
|
+
toolResultRows = [];
|
|
348
|
+
assistantRow = makeAssistantRow([
|
|
349
|
+
{ type: "text", text: "Here is the digest." },
|
|
350
|
+
] as ContentBlock[]);
|
|
351
|
+
|
|
352
|
+
await run();
|
|
353
|
+
|
|
354
|
+
expect(emitCalls).toHaveLength(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("notifies after a Web API post, whose success cannot prove Slack accepted it", async () => {
|
|
358
|
+
// Slack refuses a call with HTTP 200 and `ok: false`, so a successful bash
|
|
359
|
+
// result is no evidence the post landed. This is the best case the route
|
|
360
|
+
// can produce, and it still gets the fallback.
|
|
266
361
|
turnRows = [
|
|
267
362
|
makeToolCallRow("bash", {
|
|
268
363
|
command:
|
|
269
364
|
'assistant oauth request --provider slack_channel /chat.postMessage --json \'{"channel":"C1","text":"digest"}\'',
|
|
270
365
|
}),
|
|
271
366
|
];
|
|
367
|
+
toolResultRows = [makeToolResultRow("bash")];
|
|
272
368
|
assistantRow = makeAssistantRow([
|
|
273
369
|
{ type: "text", text: "Posted the digest to #general." },
|
|
274
370
|
] as ContentBlock[]);
|
|
275
371
|
|
|
276
372
|
await run();
|
|
277
373
|
|
|
278
|
-
expect(emitCalls).toHaveLength(
|
|
374
|
+
expect(emitCalls).toHaveLength(1);
|
|
279
375
|
});
|
|
280
376
|
|
|
281
377
|
test("still notifies when the run's tool calls were not deliveries", async () => {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import type { AssistantEvent } from "../../api/index.js";
|
|
22
22
|
import type { InterfaceId } from "../../channels/types.js";
|
|
23
|
+
import { getAssistantName } from "../../daemon/identity-helpers.js";
|
|
23
24
|
import { updateMessageContent } from "../../persistence/conversation-crud.js";
|
|
24
25
|
import { publishConversationMessagesChanged } from "../../runtime/sync/resource-sync-events.js";
|
|
25
26
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -101,12 +102,14 @@ export class VellumAdapter implements ChannelAdapter {
|
|
|
101
102
|
|
|
102
103
|
const silent =
|
|
103
104
|
payload.urgency !== "high" && payload.urgency !== "critical";
|
|
105
|
+
const assistantName = getAssistantName()?.trim() || undefined;
|
|
104
106
|
|
|
105
107
|
this.broadcast({
|
|
106
108
|
type: "notification_intent",
|
|
107
109
|
deliveryId: payload.deliveryId,
|
|
108
110
|
correlationId: payload.correlationId,
|
|
109
111
|
sourceEventName: payload.sourceEventName,
|
|
112
|
+
...(assistantName ? { assistantName } : {}),
|
|
110
113
|
title: payload.copy.title,
|
|
111
114
|
body: payload.copy.body,
|
|
112
115
|
deepLinkMetadata: payload.deepLinkTarget,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getMessageById,
|
|
23
23
|
parseMessageMetadata,
|
|
24
24
|
} from "../persistence/conversation-crud.js";
|
|
25
|
+
import { isReplaceableTitle } from "../persistence/conversation-title-placeholders.js";
|
|
25
26
|
import {
|
|
26
27
|
isDesktopOriginatedUserMessage,
|
|
27
28
|
isReplyPushIneligibleUserMessage,
|
|
@@ -247,9 +248,13 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
247
248
|
// lock screen. Absent `requestedTitle` lets the decision branch derive a
|
|
248
249
|
// title from the body, which reads better than an empty or placeholder
|
|
249
250
|
// conversation title.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
//
|
|
252
|
+
// Placeholder titles are plausible non-empty strings and survive
|
|
253
|
+
// sanitizing. They count as absent so the body supplies the title instead.
|
|
254
|
+
const storedTitle = conversation.title?.trim() ?? "";
|
|
255
|
+
const requestedTitle = isReplaceableTitle(storedTitle)
|
|
256
|
+
? ""
|
|
257
|
+
: sanitizeNotificationTitle(flattenTitleWhitespace(storedTitle));
|
|
253
258
|
|
|
254
259
|
// Read as close to the emit as possible: nothing short-circuits on it.
|
|
255
260
|
// Presence only speaks for a turn the desktop itself opened, on that row's
|