@vellumai/assistant 0.11.7 → 0.11.8-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/ARCHITECTURE.md +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
afterEach,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
mock,
|
|
11
|
+
spyOn,
|
|
12
|
+
test,
|
|
13
|
+
} from "bun:test";
|
|
14
|
+
|
|
15
|
+
// The feed writer publishes `home_feed_updated` on every write; the hub is
|
|
16
|
+
// stubbed so tests run without an SSE server. Mocked before the writer
|
|
17
|
+
// module is imported so the import picks it up.
|
|
18
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
19
|
+
assistantEventHub: {
|
|
20
|
+
publish: async () => {},
|
|
21
|
+
subscribe: () => () => {},
|
|
22
|
+
},
|
|
23
|
+
broadcastMessage: () => {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Gateway reads used by reconciliation. Stubs are swapped per test; the
|
|
27
|
+
// spread keeps every other export real so transitive importers keep
|
|
28
|
+
// working (partial factories break at import time).
|
|
29
|
+
const actualGateway =
|
|
30
|
+
await import("../../channels/gateway-guardian-requests.js");
|
|
31
|
+
let listGuardianRequestsStub: () => Promise<unknown[]> = async () => [];
|
|
32
|
+
let getGuardianRequestStub: (id: string) => Promise<unknown> = async () => null;
|
|
33
|
+
mock.module("../../channels/gateway-guardian-requests.js", () => ({
|
|
34
|
+
...actualGateway,
|
|
35
|
+
listGuardianRequests: (...args: unknown[]) =>
|
|
36
|
+
(listGuardianRequestsStub as (...a: unknown[]) => Promise<unknown[]>)(
|
|
37
|
+
...args,
|
|
38
|
+
),
|
|
39
|
+
getGuardianRequest: (id: string) => getGuardianRequestStub(id),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
buildPendingGuardianProjection,
|
|
44
|
+
guardianFeedItemId,
|
|
45
|
+
reconcileGuardianFeedProjections,
|
|
46
|
+
requestIdFromGuardianFeedItemId,
|
|
47
|
+
writeGuardianFeedReceipt,
|
|
48
|
+
} = await import("../guardian-feed-projection.js");
|
|
49
|
+
const { appendFeedItem, bulkSetFeedItemStatus, getHomeFeedPath, readHomeFeed } =
|
|
50
|
+
await import("../../home/feed-writer.js");
|
|
51
|
+
const { isPendingGuardianFeedItem } =
|
|
52
|
+
await import("../../api/responses/home.js");
|
|
53
|
+
type FeedItem = import("../../api/responses/home.js").FeedItem;
|
|
54
|
+
|
|
55
|
+
let workspaceDir: string;
|
|
56
|
+
let origWorkspaceDir: string | undefined;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-gfp-"));
|
|
60
|
+
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
61
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
62
|
+
listGuardianRequestsStub = async () => [];
|
|
63
|
+
getGuardianRequestStub = async () => null;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
if (origWorkspaceDir === undefined) {
|
|
68
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
69
|
+
} else {
|
|
70
|
+
process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
|
|
71
|
+
}
|
|
72
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const toolApprovalPayload = {
|
|
76
|
+
requestKind: "tool_approval",
|
|
77
|
+
requestId: "req-1",
|
|
78
|
+
requestCode: "ABC123",
|
|
79
|
+
questionText: "Alice asked the assistant to look up an issue",
|
|
80
|
+
toolName: "linear_graphql",
|
|
81
|
+
sourceChannel: "slack",
|
|
82
|
+
sourceChatId: "C0123456789",
|
|
83
|
+
requesterIdentifier: "Alice",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function pendingGuardianItem(requestId: string): FeedItem {
|
|
87
|
+
const projection = buildPendingGuardianProjection({
|
|
88
|
+
...toolApprovalPayload,
|
|
89
|
+
requestId,
|
|
90
|
+
});
|
|
91
|
+
if (!projection) {
|
|
92
|
+
throw new Error("projection should build");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id: guardianFeedItemId(requestId),
|
|
96
|
+
type: "notification",
|
|
97
|
+
priority: 50,
|
|
98
|
+
summary: "Alice asked the assistant to look up an issue",
|
|
99
|
+
timestamp: "2026-08-31T12:00:00.000Z",
|
|
100
|
+
createdAt: "2026-08-31T12:00:00.000Z",
|
|
101
|
+
status: "new",
|
|
102
|
+
urgency: "high",
|
|
103
|
+
guardianRequest: projection,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("buildPendingGuardianProjection", () => {
|
|
108
|
+
test("tool approval projects as a pending approval with source facts", () => {
|
|
109
|
+
const projection = buildPendingGuardianProjection(toolApprovalPayload);
|
|
110
|
+
expect(projection).toMatchObject({
|
|
111
|
+
requestId: "req-1",
|
|
112
|
+
kind: "tool_approval",
|
|
113
|
+
intent: "approval",
|
|
114
|
+
status: "pending",
|
|
115
|
+
requesterLabel: "Alice",
|
|
116
|
+
toolName: "linear_graphql",
|
|
117
|
+
sourceChannel: "slack",
|
|
118
|
+
sourceContextLabel: "Slack #C0123456789",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a question without a tool projects as question intent", () => {
|
|
123
|
+
const projection = buildPendingGuardianProjection({
|
|
124
|
+
requestKind: "pending_question",
|
|
125
|
+
requestId: "req-q",
|
|
126
|
+
requestCode: "Q1",
|
|
127
|
+
questionText: "Which venue should I book?",
|
|
128
|
+
});
|
|
129
|
+
expect(projection?.intent).toBe("question");
|
|
130
|
+
expect(projection?.status).toBe("pending");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a sent Slack delivery yields the card deep link", () => {
|
|
134
|
+
const projection = buildPendingGuardianProjection(toolApprovalPayload, {
|
|
135
|
+
channel: "slack",
|
|
136
|
+
destination: "D0AAAAAAAAA",
|
|
137
|
+
status: "sent",
|
|
138
|
+
messageId: "1725100000.000100",
|
|
139
|
+
});
|
|
140
|
+
expect(projection?.slackCardUrl).toContain("D0AAAAAAAAA");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a payload without a requestId projects nothing", () => {
|
|
144
|
+
expect(
|
|
145
|
+
buildPendingGuardianProjection({ requestKind: "tool_approval" }),
|
|
146
|
+
).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("an access-request payload projects with the event's implied kind", () => {
|
|
150
|
+
const projection = buildPendingGuardianProjection(
|
|
151
|
+
{
|
|
152
|
+
requestId: "req-a",
|
|
153
|
+
requestCode: "AC1234",
|
|
154
|
+
sourceChannel: "telegram",
|
|
155
|
+
senderIdentifier: "Alice",
|
|
156
|
+
},
|
|
157
|
+
undefined,
|
|
158
|
+
"access_request",
|
|
159
|
+
);
|
|
160
|
+
expect(projection).toMatchObject({
|
|
161
|
+
requestId: "req-a",
|
|
162
|
+
kind: "access_request",
|
|
163
|
+
intent: "approval",
|
|
164
|
+
status: "pending",
|
|
165
|
+
requesterLabel: "Alice",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("guardianFeedItemId", () => {
|
|
171
|
+
test("round-trips through requestIdFromGuardianFeedItemId", () => {
|
|
172
|
+
expect(requestIdFromGuardianFeedItemId(guardianFeedItemId("req-9"))).toBe(
|
|
173
|
+
"req-9",
|
|
174
|
+
);
|
|
175
|
+
expect(requestIdFromGuardianFeedItemId("notif:xyz")).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("bulk-dismiss protection", () => {
|
|
180
|
+
test("clear-all skips the pending item and clears its receipt", async () => {
|
|
181
|
+
await appendFeedItem(pendingGuardianItem("req-1"));
|
|
182
|
+
await appendFeedItem({
|
|
183
|
+
id: "notif:other",
|
|
184
|
+
type: "notification",
|
|
185
|
+
priority: 50,
|
|
186
|
+
summary: "routine",
|
|
187
|
+
timestamp: "2026-08-31T12:00:00.000Z",
|
|
188
|
+
createdAt: "2026-08-31T12:00:00.000Z",
|
|
189
|
+
status: "seen",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const first = await bulkSetFeedItemStatus(
|
|
193
|
+
["new", "seen", "acted_on"],
|
|
194
|
+
"dismissed",
|
|
195
|
+
);
|
|
196
|
+
let items = readHomeFeed().items;
|
|
197
|
+
const guardianItem = items.find(
|
|
198
|
+
(i) => i.id === guardianFeedItemId("req-1"),
|
|
199
|
+
);
|
|
200
|
+
// Exactly the routine item flipped; the pending projection is intact
|
|
201
|
+
// and still actionable.
|
|
202
|
+
expect(first).toBe(1);
|
|
203
|
+
expect(guardianItem?.status).toBe("new");
|
|
204
|
+
expect(items.find((i) => i.id === "notif:other")?.status).toBe("dismissed");
|
|
205
|
+
expect(guardianItem && isPendingGuardianFeedItem(guardianItem)).toBe(true);
|
|
206
|
+
|
|
207
|
+
// Sensitivity: once the request resolves, the same bulk op clears it.
|
|
208
|
+
await writeGuardianFeedReceipt({ requestId: "req-1", status: "approved" });
|
|
209
|
+
const second = await bulkSetFeedItemStatus(
|
|
210
|
+
["new", "seen", "acted_on"],
|
|
211
|
+
"dismissed",
|
|
212
|
+
);
|
|
213
|
+
items = readHomeFeed().items;
|
|
214
|
+
expect(second).toBe(1);
|
|
215
|
+
expect(
|
|
216
|
+
items.find((i) => i.id === guardianFeedItemId("req-1"))?.status,
|
|
217
|
+
).toBe("dismissed");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("bulk read-marking never dismisses but may still touch guardian items", async () => {
|
|
221
|
+
await appendFeedItem(pendingGuardianItem("req-2"));
|
|
222
|
+
const updated = await bulkSetFeedItemStatus(["new"], "seen");
|
|
223
|
+
// Only dismissal is protected; a bulk `seen` transition (not offered
|
|
224
|
+
// for guardian rows by the web client, which excludes their ids) is
|
|
225
|
+
// not blocked by the writer.
|
|
226
|
+
expect(updated).toBe(1);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("writeGuardianFeedReceipt", () => {
|
|
231
|
+
test("a decision rewrites the item into a clearable receipt", async () => {
|
|
232
|
+
await appendFeedItem(pendingGuardianItem("req-3"));
|
|
233
|
+
const ok = await writeGuardianFeedReceipt({
|
|
234
|
+
requestId: "req-3",
|
|
235
|
+
status: "approved",
|
|
236
|
+
decidedAction: "approve_once",
|
|
237
|
+
decidedAtMs: Date.parse("2026-08-31T13:00:00.000Z"),
|
|
238
|
+
});
|
|
239
|
+
expect(ok).toBe(true);
|
|
240
|
+
|
|
241
|
+
const item = readHomeFeed().items.find(
|
|
242
|
+
(i) => i.id === guardianFeedItemId("req-3"),
|
|
243
|
+
);
|
|
244
|
+
expect(item?.guardianRequest).toMatchObject({
|
|
245
|
+
status: "approved",
|
|
246
|
+
decidedAction: "approve_once",
|
|
247
|
+
decidedAt: "2026-08-31T13:00:00.000Z",
|
|
248
|
+
});
|
|
249
|
+
expect(item?.urgency).toBe("medium");
|
|
250
|
+
expect(item && isPendingGuardianFeedItem(item)).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a superseded auto-deny carries its reason and no decider", async () => {
|
|
254
|
+
await appendFeedItem(pendingGuardianItem("req-4"));
|
|
255
|
+
await writeGuardianFeedReceipt({
|
|
256
|
+
requestId: "req-4",
|
|
257
|
+
status: "denied",
|
|
258
|
+
decidedAction: "reject",
|
|
259
|
+
terminalReason: "superseded",
|
|
260
|
+
});
|
|
261
|
+
const item = readHomeFeed().items.find(
|
|
262
|
+
(i) => i.id === guardianFeedItemId("req-4"),
|
|
263
|
+
);
|
|
264
|
+
expect(item?.guardianRequest?.terminalReason).toBe("superseded");
|
|
265
|
+
expect(item?.guardianRequest?.decidedByLabel).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a request with no item resolves true (nothing to retry)", async () => {
|
|
269
|
+
expect(
|
|
270
|
+
await writeGuardianFeedReceipt({ requestId: "ghost", status: "expired" }),
|
|
271
|
+
).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a lost write on an existing item resolves false so callers retry", async () => {
|
|
275
|
+
await appendFeedItem(pendingGuardianItem("req-5"));
|
|
276
|
+
// Fail the next feed-file write while the item is still present in
|
|
277
|
+
// the read path, so the missing-item and lost-write cases separate.
|
|
278
|
+
const feedPath = getHomeFeedPath();
|
|
279
|
+
const originalWrite = fs.writeFileSync;
|
|
280
|
+
const spy = spyOn(fs, "writeFileSync").mockImplementation(((
|
|
281
|
+
path: fs.PathOrFileDescriptor,
|
|
282
|
+
data: string | NodeJS.ArrayBufferView,
|
|
283
|
+
options?: fs.WriteFileOptions,
|
|
284
|
+
) => {
|
|
285
|
+
if (typeof path === "string" && path === feedPath) {
|
|
286
|
+
throw new Error("Simulated write failure");
|
|
287
|
+
}
|
|
288
|
+
return originalWrite(path, data, options);
|
|
289
|
+
}) as typeof fs.writeFileSync);
|
|
290
|
+
try {
|
|
291
|
+
expect(
|
|
292
|
+
await writeGuardianFeedReceipt({
|
|
293
|
+
requestId: "req-5",
|
|
294
|
+
status: "expired",
|
|
295
|
+
}),
|
|
296
|
+
).toBe(false);
|
|
297
|
+
} finally {
|
|
298
|
+
spy.mockRestore();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("reconcileGuardianFeedProjections", () => {
|
|
304
|
+
const wireRequest = (overrides: Record<string, unknown>) => ({
|
|
305
|
+
id: "req-r",
|
|
306
|
+
kind: "tool_approval",
|
|
307
|
+
sourceType: "channel",
|
|
308
|
+
sourceChannel: "slack",
|
|
309
|
+
sourceConversationId: "conv-1",
|
|
310
|
+
requesterExternalUserId: "U1",
|
|
311
|
+
requesterChatId: null,
|
|
312
|
+
guardianExternalUserId: null,
|
|
313
|
+
guardianPrincipalId: null,
|
|
314
|
+
callSessionId: null,
|
|
315
|
+
pendingQuestionId: null,
|
|
316
|
+
questionText: "Alice asked the assistant to look up an issue",
|
|
317
|
+
requestCode: "ABC123",
|
|
318
|
+
toolName: "linear_graphql",
|
|
319
|
+
inputDigest: null,
|
|
320
|
+
commandPreview: null,
|
|
321
|
+
riskLevel: null,
|
|
322
|
+
activityText: null,
|
|
323
|
+
executionTarget: null,
|
|
324
|
+
requesterSignals: null,
|
|
325
|
+
requestTrigger: null,
|
|
326
|
+
status: "pending",
|
|
327
|
+
answerText: null,
|
|
328
|
+
decidedByExternalUserId: null,
|
|
329
|
+
decidedByPrincipalId: null,
|
|
330
|
+
followupState: null,
|
|
331
|
+
expiresAt: null,
|
|
332
|
+
createdAt: Date.parse("2026-08-31T12:00:00.000Z"),
|
|
333
|
+
updatedAt: Date.parse("2026-08-31T12:00:00.000Z"),
|
|
334
|
+
...overrides,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("backfills an item for a pending request that has none", async () => {
|
|
338
|
+
listGuardianRequestsStub = async () => [wireRequest({ id: "req-r" })];
|
|
339
|
+
await reconcileGuardianFeedProjections();
|
|
340
|
+
const item = readHomeFeed().items.find(
|
|
341
|
+
(i) => i.id === guardianFeedItemId("req-r"),
|
|
342
|
+
);
|
|
343
|
+
expect(item?.guardianRequest?.status).toBe("pending");
|
|
344
|
+
expect(item?.conversationId).toBe("conv-1");
|
|
345
|
+
expect(item?.detailPanel?.kind).toBe("permissionChat");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("receipts an actionable item whose request went terminal", async () => {
|
|
349
|
+
await appendFeedItem(pendingGuardianItem("req-t"));
|
|
350
|
+
listGuardianRequestsStub = async () => [];
|
|
351
|
+
getGuardianRequestStub = async () =>
|
|
352
|
+
wireRequest({ id: "req-t", status: "denied" });
|
|
353
|
+
await reconcileGuardianFeedProjections();
|
|
354
|
+
const item = readHomeFeed().items.find(
|
|
355
|
+
(i) => i.id === guardianFeedItemId("req-t"),
|
|
356
|
+
);
|
|
357
|
+
expect(item?.guardianRequest?.status).toBe("denied");
|
|
358
|
+
expect(item && isPendingGuardianFeedItem(item)).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -40,7 +40,13 @@ mock.module("../../runtime/sync/resource-sync-events.js", () => ({
|
|
|
40
40
|
},
|
|
41
41
|
}));
|
|
42
42
|
|
|
43
|
+
// Both factories spread the actual module: the module under test reaches
|
|
44
|
+
// feed-writer through `guardian-feed-projection` too, and the real
|
|
45
|
+
// feed-writer needs `parseFeedFile` from feed-types, so a partial factory
|
|
46
|
+
// would fail at import time on any export it omitted.
|
|
47
|
+
const actualFeedTypes = await import("../../home/feed-types.js");
|
|
43
48
|
mock.module("../../home/feed-types.js", () => ({
|
|
49
|
+
...actualFeedTypes,
|
|
44
50
|
feedItemSchema: {
|
|
45
51
|
parse: (item: unknown) => {
|
|
46
52
|
if (feedItemSchemaShouldReject) {
|
|
@@ -51,7 +57,9 @@ mock.module("../../home/feed-types.js", () => ({
|
|
|
51
57
|
},
|
|
52
58
|
}));
|
|
53
59
|
|
|
60
|
+
const actualFeedWriter = await import("../../home/feed-writer.js");
|
|
54
61
|
mock.module("../../home/feed-writer.js", () => ({
|
|
62
|
+
...actualFeedWriter,
|
|
55
63
|
appendFeedItem: async (item: FeedItem) => {
|
|
56
64
|
appendCalls.push(item);
|
|
57
65
|
},
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* (cached per user) and a room id on the binding can never become a delivery
|
|
9
9
|
* target. That is Slack's D-prefix DM gate, achieved by construction.
|
|
10
10
|
*
|
|
11
|
-
* Approval notifications
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Approval notifications render component buttons whose presses the gateway
|
|
12
|
+
* ingests as button events on the shared `apr:` callback convention; a
|
|
13
|
+
* rendering failure falls back to the plain-text card with typed-command
|
|
14
|
+
* instructions, so a card is always actionable one way or the other.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { openDiscordDmChannel } from "../../messaging/providers/discord/api.js";
|
|
18
18
|
import {
|
|
19
|
+
DiscordPartialSendError,
|
|
19
20
|
editDiscordMessage,
|
|
20
21
|
sendDiscordReply,
|
|
21
22
|
} from "../../messaging/providers/discord/send.js";
|
|
@@ -53,14 +54,66 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
payload.approvalContext,
|
|
59
|
-
);
|
|
57
|
+
const messageText = resolveMessageText(payload);
|
|
58
|
+
const approval = payload.approvalContext;
|
|
60
59
|
|
|
61
60
|
try {
|
|
62
61
|
const channelId = await openDiscordDmChannel(guardianUserId);
|
|
63
|
-
|
|
62
|
+
|
|
63
|
+
if (approval) {
|
|
64
|
+
// Attempt rich delivery with component buttons; on failure, fall
|
|
65
|
+
// back to the plain-text card below.
|
|
66
|
+
try {
|
|
67
|
+
const sent = await sendDiscordReply(
|
|
68
|
+
{ channelId },
|
|
69
|
+
messageText,
|
|
70
|
+
approval,
|
|
71
|
+
);
|
|
72
|
+
log.info(
|
|
73
|
+
{ sourceEventName: payload.sourceEventName, guardianUserId },
|
|
74
|
+
"Discord approval notification delivered with buttons",
|
|
75
|
+
);
|
|
76
|
+
// The message id lets the delivery row address the card later
|
|
77
|
+
// (reactions, button presses, in-place withdrawal).
|
|
78
|
+
return { success: true, messageId: sent.lastMessageId };
|
|
79
|
+
} catch (richErr) {
|
|
80
|
+
if (richErr instanceof DiscordPartialSendError) {
|
|
81
|
+
// The leading chunks are delivered and cannot be unsent, so a
|
|
82
|
+
// full plain-text fallback would duplicate them. Complete the
|
|
83
|
+
// card instead: the undelivered remainder plus the typed-command
|
|
84
|
+
// instructions, whose message becomes the card's address.
|
|
85
|
+
log.warn(
|
|
86
|
+
{
|
|
87
|
+
err: richErr,
|
|
88
|
+
sourceEventName: payload.sourceEventName,
|
|
89
|
+
guardianUserId,
|
|
90
|
+
chunksSent: richErr.chunksSent,
|
|
91
|
+
},
|
|
92
|
+
"Rich Discord delivery failed mid-card, completing in plain text",
|
|
93
|
+
);
|
|
94
|
+
const completion = await sendDiscordReply(
|
|
95
|
+
{ channelId },
|
|
96
|
+
appendPlainTextFallback(richErr.remainingText, approval),
|
|
97
|
+
);
|
|
98
|
+
return { success: true, messageId: completion.lastMessageId };
|
|
99
|
+
}
|
|
100
|
+
log.warn(
|
|
101
|
+
{
|
|
102
|
+
err: richErr,
|
|
103
|
+
sourceEventName: payload.sourceEventName,
|
|
104
|
+
guardianUserId,
|
|
105
|
+
},
|
|
106
|
+
"Rich Discord delivery failed, falling back to plain text",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// When falling back from rich delivery, append the plain-text
|
|
112
|
+
// instructions so the guardian still knows how to approve/reject.
|
|
113
|
+
const sent = await sendDiscordReply(
|
|
114
|
+
{ channelId },
|
|
115
|
+
appendPlainTextFallback(messageText, approval),
|
|
116
|
+
);
|
|
64
117
|
|
|
65
118
|
log.info(
|
|
66
119
|
{ sourceEventName: payload.sourceEventName, guardianUserId },
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic notification conversation pairing.
|
|
3
3
|
*
|
|
4
|
-
* Materializes a conversation + message for
|
|
5
|
-
* before the adapter sends it
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Materializes a conversation + message for a notification delivery
|
|
5
|
+
* before the adapter sends it, so the delivery has an auditable
|
|
6
|
+
* conversation trail and the macOS/iOS client can deep-link into it.
|
|
7
|
+
* Guardian-request deliveries to channels are the exception: they are
|
|
8
|
+
* delivery projections of a canonical request and pair nothing (see the
|
|
9
|
+
* guard below); only their vellum delivery carries a conversation.
|
|
8
10
|
*
|
|
9
11
|
* Resolution order:
|
|
10
12
|
* 1. Explicit `reuse_existing` conversation action — highest precedence.
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
composeConversationSeed,
|
|
38
40
|
isConversationSeedSane,
|
|
39
41
|
} from "./conversation-seed-composer.js";
|
|
42
|
+
import { isGuardianRequestSignalEvent } from "./guardian-feed-projection.js";
|
|
40
43
|
import type { NotificationSignal } from "./signal.js";
|
|
41
44
|
import type {
|
|
42
45
|
ConversationAction,
|
|
@@ -122,6 +125,30 @@ export async function pairDeliveryWithConversation(
|
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// A channel-delivered guardian-request approval card (tool
|
|
129
|
+
// approvals, questions, and access requests alike) is a delivery
|
|
130
|
+
// projection of a canonical guardian request, not conversation
|
|
131
|
+
// content: its in-app homes are the home-feed "Needs attention"
|
|
132
|
+
// item and the source conversation's card (the vellum delivery,
|
|
133
|
+
// which still pairs below). Pairing a channel card here would
|
|
134
|
+
// either write it into the guardian's bound chat transcript or
|
|
135
|
+
// mint a fresh conversation for a transient work item, so it gets
|
|
136
|
+
// neither a row nor a conversation. The gateway delivery row (chat
|
|
137
|
+
// id + channel-native message id) remains its only persisted
|
|
138
|
+
// envelope.
|
|
139
|
+
if (
|
|
140
|
+
isGuardianRequestSignalEvent(signal.sourceEventName) &&
|
|
141
|
+
channel !== "vellum"
|
|
142
|
+
) {
|
|
143
|
+
return {
|
|
144
|
+
conversationId: null,
|
|
145
|
+
messageId: null,
|
|
146
|
+
strategy,
|
|
147
|
+
createdNewConversation: false,
|
|
148
|
+
conversationFallbackUsed: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
const conversationAction = options?.conversationAction;
|
|
126
153
|
const bindingContext = options?.bindingContext;
|
|
127
154
|
|
|
@@ -547,11 +547,14 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
547
547
|
const vellumDelivery = dispatchResult.deliveryResults.find(
|
|
548
548
|
(r) => r.channel === "vellum",
|
|
549
549
|
);
|
|
550
|
-
await writeHomeFeedItemForSignal(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
550
|
+
await writeHomeFeedItemForSignal(
|
|
551
|
+
signal,
|
|
552
|
+
decision,
|
|
553
|
+
vellumDelivery,
|
|
554
|
+
dispatchResult.deliveryResults,
|
|
555
|
+
).catch((err) => {
|
|
556
|
+
log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
|
|
557
|
+
});
|
|
555
558
|
|
|
556
559
|
log.info(
|
|
557
560
|
{
|
|
@@ -20,8 +20,11 @@
|
|
|
20
20
|
* read it back.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { DELIVERY_STATUS } from "@vellumai/gateway-client";
|
|
24
|
+
|
|
23
25
|
import {
|
|
24
26
|
createGuardianRequestDelivery,
|
|
27
|
+
getGuardianRequestOrNull,
|
|
25
28
|
type GuardianRequestDeliveryWire,
|
|
26
29
|
updateGuardianRequestDelivery,
|
|
27
30
|
} from "../channels/gateway-guardian-requests.js";
|
|
@@ -47,7 +50,7 @@ export interface ApprovalCardDeliveryAddress {
|
|
|
47
50
|
chatId?: string;
|
|
48
51
|
/** Channel-native message id (e.g. Slack `ts`) — the reaction/withdrawal key. */
|
|
49
52
|
messageId?: string;
|
|
50
|
-
/** Initial delivery status (defaults to
|
|
53
|
+
/** Initial delivery status (defaults to `DELIVERY_STATUS.pending`). */
|
|
51
54
|
status?: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -91,14 +94,17 @@ export async function recordApprovalCardDelivery(
|
|
|
91
94
|
* that row's id as `vellumDeliveryId` and it is reused (only its status applied)
|
|
92
95
|
* — otherwise the vellum row is created here from the result.
|
|
93
96
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
+
* The vellum result records the internal `conversationId` its card is shown
|
|
98
|
+
* in. Channel guardian cards pair no conversation (they are delivery
|
|
99
|
+
* projections; see `conversation-pairing.ts`), so their rows carry no
|
|
100
|
+
* `conversationId`. Platform push results are skipped entirely: a push
|
|
97
101
|
* has no channel-native card surface to address back to.
|
|
98
102
|
* Channel results additionally carry the chat (`destination`) and channel-native
|
|
99
103
|
* id (`messageId`) used to match inbound replies/reactions; a blank `destination`
|
|
100
104
|
* is recorded as unknown rather than persisting the literal channel name as a
|
|
101
|
-
* chat id. Status
|
|
105
|
+
* chat id. Status has two readers: the voice guardian-action sweep acts only
|
|
106
|
+
* on `sent`/`pending` rows, and card withdrawal skips rows already marked
|
|
107
|
+
* `withdrawn` (its per-surface receipt). Addressing lookups ignore status.
|
|
102
108
|
*
|
|
103
109
|
* Best-effort like the create: a status-patch failure is logged, not thrown.
|
|
104
110
|
*
|
|
@@ -146,7 +152,10 @@ export async function recordGuardianRequestDeliveries(params: {
|
|
|
146
152
|
if (deliveryId) {
|
|
147
153
|
try {
|
|
148
154
|
await updateGuardianRequestDelivery(deliveryId, {
|
|
149
|
-
status:
|
|
155
|
+
status:
|
|
156
|
+
result.status === "sent"
|
|
157
|
+
? DELIVERY_STATUS.sent
|
|
158
|
+
: DELIVERY_STATUS.failed,
|
|
150
159
|
});
|
|
151
160
|
} catch (err) {
|
|
152
161
|
log.error(
|
|
@@ -157,5 +166,37 @@ export async function recordGuardianRequestDeliveries(params: {
|
|
|
157
166
|
}
|
|
158
167
|
}
|
|
159
168
|
|
|
169
|
+
await withdrawIfRequestAlreadyTerminal(requestId);
|
|
170
|
+
|
|
160
171
|
return vellumDeliveryId;
|
|
161
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Close the delivery/decision race: recording happens after the notification
|
|
176
|
+
* pipeline finishes, so a guardian who acts on a card the moment it lands can
|
|
177
|
+
* resolve the request before its delivery rows exist. That decision's
|
|
178
|
+
* withdrawal pass then finds nothing to withdraw, and the rows recorded here
|
|
179
|
+
* would describe live cards for an already-terminal request. Re-running
|
|
180
|
+
* withdrawal after recording converges them; rows a prior pass already
|
|
181
|
+
* withdrew are skipped, so the overlap never re-edits a surface.
|
|
182
|
+
*
|
|
183
|
+
* Best-effort like the rest of the recorder; the decided action isn't
|
|
184
|
+
* recoverable here, so denied outcomes render the plain status word.
|
|
185
|
+
*/
|
|
186
|
+
async function withdrawIfRequestAlreadyTerminal(
|
|
187
|
+
requestId: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const request = await getGuardianRequestOrNull(requestId);
|
|
190
|
+
if (!request || request.status === "pending") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Imported at call time: the withdrawal module reaches the daemon's
|
|
194
|
+
// conversation-surface graph, and a static import here would put this
|
|
195
|
+
// recorder (imported by the guardian-request producers) into that cycle.
|
|
196
|
+
const { withdrawGuardianRequestCards } =
|
|
197
|
+
await import("../approvals/guardian-card-withdrawal.js");
|
|
198
|
+
await withdrawGuardianRequestCards({
|
|
199
|
+
request,
|
|
200
|
+
status: request.status,
|
|
201
|
+
});
|
|
202
|
+
}
|