@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
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guardian request expiry sweep.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Each round reads a bounded batch of pending requests past their
|
|
5
|
+
* `expiresAt` and, per request, runs the expiry side effects (withdrawing
|
|
6
|
+
* the approval cards on every surface, notifying the requester, releasing
|
|
7
|
+
* any in-memory pending interaction), and only then confirms with the
|
|
8
|
+
* gateway's per-request expire CAS. The status flip is the receipt that the
|
|
9
|
+
* side effects ran, not the announcement that they should: a lost IPC
|
|
10
|
+
* response, a timeout, or a crash at any point leaves the row pending and
|
|
11
|
+
* past-deadline, so the next round lists it again and re-runs its fan-out.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* That order is safe because every decision path checks `expiresAt` before
|
|
14
|
+
* any write: a past-deadline row is undecidable while it waits for its
|
|
15
|
+
* fan-out. The cost is at-least-once side effects: a round that dies
|
|
16
|
+
* between the notice and the CAS re-notifies the requester next round,
|
|
17
|
+
* which is the correct trade against silently losing the withdrawal and the
|
|
18
|
+
* notice, since a stale card is an actionable control on every surface that
|
|
19
|
+
* renders buttons.
|
|
20
|
+
*
|
|
21
|
+
* Requester notices are delivered straight to the requester's channel, not
|
|
22
|
+
* the guardian-facing notification pipeline, and the guardian stays
|
|
23
|
+
* passive, since the withdrawn card already reflects expiry.
|
|
16
24
|
*
|
|
17
25
|
* Unreachable-gateway posture: log and skip the round — the next tick
|
|
18
26
|
* retries, and expiry only ever moves forward.
|
|
@@ -21,9 +29,11 @@
|
|
|
21
29
|
import { withdrawGuardianRequestCards } from "../../approvals/guardian-card-withdrawal.js";
|
|
22
30
|
import { notifyExpiredGuardianRequest } from "../../approvals/guardian-expiry-notifier.js";
|
|
23
31
|
import {
|
|
32
|
+
expireGuardianRequest,
|
|
24
33
|
type GuardianRequestWire,
|
|
25
|
-
|
|
34
|
+
listExpiredPendingGuardianRequests,
|
|
26
35
|
} from "../../channels/gateway-guardian-requests.js";
|
|
36
|
+
import { reconcileGuardianFeedProjections } from "../../notifications/guardian-feed-projection.js";
|
|
27
37
|
import { getLogger } from "../../util/logger.js";
|
|
28
38
|
|
|
29
39
|
const log = getLogger("guardian-expiry-sweep");
|
|
@@ -31,6 +41,12 @@ const log = getLogger("guardian-expiry-sweep");
|
|
|
31
41
|
/** Interval at which the expiry sweep runs (60 seconds). */
|
|
32
42
|
const SWEEP_INTERVAL_MS = 60_000;
|
|
33
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Requests handled per round. Bounds every round's IPC payload and fan-out
|
|
46
|
+
* however large a backlog grows; the remainder drains on later rounds.
|
|
47
|
+
*/
|
|
48
|
+
const SWEEP_BATCH_LIMIT = 50;
|
|
49
|
+
|
|
34
50
|
/** Timer handle for the sweep so it can be stopped in tests and shutdown. */
|
|
35
51
|
let sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
36
52
|
|
|
@@ -38,15 +54,24 @@ let sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
38
54
|
let sweepInProgress = false;
|
|
39
55
|
|
|
40
56
|
/**
|
|
41
|
-
* Run one expiry sweep round:
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
57
|
+
* Run one expiry sweep round: read a bounded batch of past-deadline pending
|
|
58
|
+
* requests, run each one's side effects, then confirm each with the
|
|
59
|
+
* gateway's per-request expire CAS (a concurrent decision that wins the
|
|
60
|
+
* race is never overwritten; the CAS requires `pending`). Returns the
|
|
61
|
+
* count of requests whose full cycle completed.
|
|
45
62
|
*/
|
|
46
63
|
export async function runGuardianExpirySweep(): Promise<number> {
|
|
47
|
-
|
|
64
|
+
// Converge the home-feed projection against canonical state on the
|
|
65
|
+
// same cadence: backfill items for pending requests that lost (or
|
|
66
|
+
// predate) theirs, and receipt any item still actionable for a
|
|
67
|
+
// terminal request. Never blocks expiry.
|
|
68
|
+
await reconcileGuardianFeedProjections().catch((err) => {
|
|
69
|
+
log.warn({ err }, "Guardian feed reconciliation failed");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stale: GuardianRequestWire[];
|
|
48
73
|
try {
|
|
49
|
-
|
|
74
|
+
stale = await listExpiredPendingGuardianRequests(SWEEP_BATCH_LIMIT);
|
|
50
75
|
} catch (err) {
|
|
51
76
|
log.warn(
|
|
52
77
|
{ err },
|
|
@@ -55,9 +80,8 @@ export async function runGuardianExpirySweep(): Promise<number> {
|
|
|
55
80
|
return 0;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for (const request of expired) {
|
|
83
|
+
let expiredCount = 0;
|
|
84
|
+
for (const request of stale) {
|
|
61
85
|
log.info(
|
|
62
86
|
{
|
|
63
87
|
event: "guardian_request_expired",
|
|
@@ -65,34 +89,68 @@ export async function runGuardianExpirySweep(): Promise<number> {
|
|
|
65
89
|
kind: request.kind,
|
|
66
90
|
expiresAt: request.expiresAt,
|
|
67
91
|
},
|
|
68
|
-
"
|
|
92
|
+
"Expiring guardian request via sweep",
|
|
69
93
|
);
|
|
70
94
|
|
|
71
95
|
// Withdraw the now-stale approval cards on every surface. No origin
|
|
72
96
|
// channel — the expiry is system-driven, so all surfaces (including
|
|
73
|
-
// in-app) are withdrawn.
|
|
74
|
-
|
|
97
|
+
// in-app) are withdrawn. Incomplete withdrawal defers the whole
|
|
98
|
+
// request: the notice has not been sent yet, so the retry next round
|
|
99
|
+
// repeats only idempotent card edits, never a delivered notice. A
|
|
100
|
+
// surface that fails persistently keeps its request in the pending set
|
|
101
|
+
// with this warning every round, so the stall is observable rather
|
|
102
|
+
// than silent.
|
|
103
|
+
const withdrawal = await withdrawGuardianRequestCards({
|
|
75
104
|
request,
|
|
76
105
|
status: "expired",
|
|
77
106
|
});
|
|
107
|
+
if (!withdrawal.complete) {
|
|
108
|
+
log.warn(
|
|
109
|
+
{ requestId: request.id },
|
|
110
|
+
"Card withdrawal incomplete; leaving the request pending for the next round",
|
|
111
|
+
);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
78
114
|
|
|
79
115
|
// Notify the requester their request expired and release any in-memory
|
|
80
|
-
// pending interaction.
|
|
81
|
-
//
|
|
82
|
-
await notifyExpiredGuardianRequest(request);
|
|
116
|
+
// pending interaction. A failed notice also defers the request: an
|
|
117
|
+
// unsent notice is exactly what a retry can still deliver.
|
|
118
|
+
const notice = await notifyExpiredGuardianRequest(request);
|
|
119
|
+
if (!notice.complete) {
|
|
120
|
+
log.warn(
|
|
121
|
+
{ requestId: request.id },
|
|
122
|
+
"Expiry notice incomplete; leaving the request pending for the next round",
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The receipt: only after the side effects does the row leave the
|
|
128
|
+
// pending set. A failure here leaves it discoverable for the next
|
|
129
|
+
// round rather than silently done; the notice-then-lost-confirmation
|
|
130
|
+
// window is the one duplicate-notice case, and it is bounded to that
|
|
131
|
+
// crash or timeout.
|
|
132
|
+
try {
|
|
133
|
+
await expireGuardianRequest(request.id);
|
|
134
|
+
expiredCount += 1;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
log.warn(
|
|
137
|
+
{ err, requestId: request.id },
|
|
138
|
+
"Expire confirmation failed; the next round re-runs this request",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
83
141
|
}
|
|
84
142
|
|
|
85
|
-
if (
|
|
143
|
+
if (expiredCount > 0) {
|
|
86
144
|
log.info(
|
|
87
145
|
{
|
|
88
146
|
event: "guardian_expiry_sweep_complete",
|
|
89
|
-
expiredCount
|
|
147
|
+
expiredCount,
|
|
90
148
|
},
|
|
91
|
-
`Guardian expiry sweep: expired ${
|
|
149
|
+
`Guardian expiry sweep: expired ${expiredCount} request(s)`,
|
|
92
150
|
);
|
|
93
151
|
}
|
|
94
152
|
|
|
95
|
-
return
|
|
153
|
+
return expiredCount;
|
|
96
154
|
}
|
|
97
155
|
|
|
98
156
|
/**
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form-agnostic half of the guardian-form IPC surface.
|
|
3
|
+
*
|
|
4
|
+
* A form's own route carries its schema and its wire events; these two are the
|
|
5
|
+
* same for every form, so a new one gets them without adding routes of its own:
|
|
6
|
+
*
|
|
7
|
+
* - `guardian_form_claim` the writer takes the form before writing
|
|
8
|
+
* - `resolve_guardian_form` the writer reports back, unblocking the command
|
|
9
|
+
*
|
|
10
|
+
* The contact forms predate these and keep their own `contact_prompt_claim` /
|
|
11
|
+
* `resolve_contact_prompt` names, because the gateway that calls them ships
|
|
12
|
+
* separately from the daemon and an older one still uses the old names. New
|
|
13
|
+
* forms should use these.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
19
|
+
import {
|
|
20
|
+
claimGuardianForm,
|
|
21
|
+
type GuardianFormResult,
|
|
22
|
+
resolveGuardianForm,
|
|
23
|
+
} from "../guardian-form-registry.js";
|
|
24
|
+
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
25
|
+
|
|
26
|
+
const RequestIdParams = z.object({
|
|
27
|
+
requestId: z.string().describe("The pending form's id."),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const ClaimParams = RequestIdParams.extend({
|
|
31
|
+
kind: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(
|
|
35
|
+
"The form the caller believes it is writing for. Rejected when it is not what the id is holding. Omitted by callers that predate the check.",
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hand a writer's report to the call parked on the form.
|
|
41
|
+
*
|
|
42
|
+
* Everything but `requestId` and `error` is passed through untouched, so a
|
|
43
|
+
* form's result is whatever its writer chose to send. An `error` is what makes
|
|
44
|
+
* it a failure; there is no other flag.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveFormFromCallback({ body = {} }: RouteHandlerArgs): {
|
|
47
|
+
resolved: boolean;
|
|
48
|
+
} {
|
|
49
|
+
const {
|
|
50
|
+
requestId,
|
|
51
|
+
error,
|
|
52
|
+
...rest
|
|
53
|
+
}: { requestId: string; error?: string } & Record<string, unknown> =
|
|
54
|
+
body as never;
|
|
55
|
+
|
|
56
|
+
const result: GuardianFormResult = error
|
|
57
|
+
? { ok: false, error }
|
|
58
|
+
: { ok: true, ...rest };
|
|
59
|
+
return resolveGuardianForm(requestId, result);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function claimForm({ body = {} }: RouteHandlerArgs): {
|
|
63
|
+
claimed: boolean;
|
|
64
|
+
reason?: "already_claimed" | "unknown" | "wrong_kind";
|
|
65
|
+
settleMs?: number;
|
|
66
|
+
} {
|
|
67
|
+
const { requestId, kind } = ClaimParams.parse(body);
|
|
68
|
+
return claimGuardianForm(requestId, kind);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CLAIM_RESPONSE = z.object({
|
|
72
|
+
claimed: z.boolean(),
|
|
73
|
+
reason: z.enum(["already_claimed", "unknown", "wrong_kind"]).optional(),
|
|
74
|
+
settleMs: z
|
|
75
|
+
.number()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe(
|
|
78
|
+
"How long a granted claim has to report its write back before the waiting call gives up.",
|
|
79
|
+
),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const GUARDIAN_FORM_ROUTES: RouteDefinition[] = [
|
|
83
|
+
{
|
|
84
|
+
operationId: "guardian_form_claim",
|
|
85
|
+
endpoint: "guardian_form_claim",
|
|
86
|
+
method: "POST",
|
|
87
|
+
policy: {
|
|
88
|
+
requiredScopes: ["settings.write"],
|
|
89
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
90
|
+
},
|
|
91
|
+
handler: claimForm,
|
|
92
|
+
summary: "Claim a pending guardian form for one submission",
|
|
93
|
+
description:
|
|
94
|
+
"Marks a pending form as answered so a second client submitting the same form writes nothing. Returns claimed=false with reason 'already_claimed' when somebody got there first, 'unknown' when no such form is pending, or 'wrong_kind' when the id belongs to a different form than the caller named. A granted claim carries the window its write has to report back in.",
|
|
95
|
+
tags: ["guardian-forms"],
|
|
96
|
+
requestBody: ClaimParams,
|
|
97
|
+
responseBody: CLAIM_RESPONSE,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
operationId: "resolve_guardian_form",
|
|
101
|
+
endpoint: "resolve_guardian_form",
|
|
102
|
+
method: "POST",
|
|
103
|
+
policy: {
|
|
104
|
+
requiredScopes: ["settings.write"],
|
|
105
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
106
|
+
},
|
|
107
|
+
handler: resolveFormFromCallback,
|
|
108
|
+
summary: "Report a guardian form's outcome back to the waiting command",
|
|
109
|
+
description:
|
|
110
|
+
"Called by the writer once it has committed (or failed). Fields other than requestId and error are passed through to the parked caller untouched; an error makes the outcome a failure.",
|
|
111
|
+
tags: ["guardian-forms"],
|
|
112
|
+
requestBody: RequestIdParams.catchall(z.unknown()),
|
|
113
|
+
responseBody: z.object({ resolved: z.boolean() }),
|
|
114
|
+
},
|
|
115
|
+
];
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* messages reach this handler.
|
|
7
7
|
*/
|
|
8
8
|
import type { SourceMetadata } from "@vellumai/gateway-client";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
resolveInboundEventKind,
|
|
11
|
+
resolveInboundReactionPayload,
|
|
12
|
+
} from "@vellumai/gateway-client";
|
|
10
13
|
import {
|
|
11
14
|
ADMISSION_POLICY_DEFAULT,
|
|
12
15
|
type AdmissionPolicy,
|
|
@@ -18,6 +21,7 @@ import {
|
|
|
18
21
|
attachmentsToContentBlocks,
|
|
19
22
|
type MessageAttachmentInput,
|
|
20
23
|
} from "../../agent/attachments.js";
|
|
24
|
+
import { listGuardianRequestDeliveriesByChat } from "../../channels/gateway-guardian-requests.js";
|
|
21
25
|
import { audienceForReader } from "../../channels/message-audience.js";
|
|
22
26
|
import {
|
|
23
27
|
CHANNEL_IDS,
|
|
@@ -88,6 +92,7 @@ import { applyDeterministicTitleIfReplaceable } from "../../persistence/conversa
|
|
|
88
92
|
import {
|
|
89
93
|
clearPayload,
|
|
90
94
|
findMessageBySourceId,
|
|
95
|
+
hasInboundEventForSource,
|
|
91
96
|
recordInbound,
|
|
92
97
|
} from "../../persistence/delivery-crud.js";
|
|
93
98
|
import { markProcessed } from "../../persistence/delivery-status.js";
|
|
@@ -120,8 +125,8 @@ import { handleGuardianActivationIntercept } from "./inbound-stages/guardian-act
|
|
|
120
125
|
import { handleGuardianReplyIntercept } from "./inbound-stages/guardian-reply-intercept.js";
|
|
121
126
|
import { prepareChannelInboundContent } from "./inbound-stages/inbound-content-prep.js";
|
|
122
127
|
import {
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
handleReactionIntercept,
|
|
129
|
+
isReactionEvent,
|
|
125
130
|
} from "./inbound-stages/reaction-intercept.js";
|
|
126
131
|
import { runSecretIngressCheck } from "./inbound-stages/secret-ingress-check.js";
|
|
127
132
|
import { tryTranscribeAudioAttachments } from "./inbound-stages/transcribe-audio.js";
|
|
@@ -372,11 +377,13 @@ export async function handleChannelInbound({
|
|
|
372
377
|
const hasCallbackData =
|
|
373
378
|
typeof body.callbackData === "string" && body.callbackData.length > 0;
|
|
374
379
|
|
|
380
|
+
// Only a plain message must carry a body: every other family refers to
|
|
381
|
+
// another message and legitimately arrives empty (a delete has no
|
|
382
|
+
// content at all).
|
|
375
383
|
if (
|
|
376
384
|
trimmedContent.length === 0 &&
|
|
377
385
|
!hasAttachments &&
|
|
378
|
-
|
|
379
|
-
!hasCallbackData
|
|
386
|
+
eventKind === "message"
|
|
380
387
|
) {
|
|
381
388
|
throw new BadRequestError("content or attachmentIds is required");
|
|
382
389
|
}
|
|
@@ -421,7 +428,7 @@ export async function handleChannelInbound({
|
|
|
421
428
|
return guardianActivationResponse;
|
|
422
429
|
}
|
|
423
430
|
|
|
424
|
-
// ──
|
|
431
|
+
// ── Reaction handling ──
|
|
425
432
|
// Reactions are passive channel signals — not messages, and not access
|
|
426
433
|
// attempts. Dispatch them to a dedicated interceptor BEFORE the message
|
|
427
434
|
// pipeline (ACL, admission floor, disk-pressure, conversation binding) so a
|
|
@@ -431,10 +438,20 @@ export async function handleChannelInbound({
|
|
|
431
438
|
// transcript signals in the conversation of the reacted message, and routes
|
|
432
439
|
// a guardian's reaction on an approval card through the guardian decision
|
|
433
440
|
// pipeline. Reactions never mint a conversation and never drive an agent
|
|
434
|
-
// turn.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
441
|
+
// turn. A family member whose payload does not resolve (no emoji or no
|
|
442
|
+
// target message id) is dropped as noise here: the kind names the family,
|
|
443
|
+
// so it must never fall through and be read as a message.
|
|
444
|
+
if (isReactionEvent(body)) {
|
|
445
|
+
const reaction = resolveInboundReactionPayload(body);
|
|
446
|
+
if (!reaction) {
|
|
447
|
+
log.debug(
|
|
448
|
+
{ sourceChannel, conversationExternalId },
|
|
449
|
+
"Dropping reaction with unresolvable payload",
|
|
450
|
+
);
|
|
451
|
+
return { accepted: true, reaction: "dropped_unresolvable_payload" };
|
|
452
|
+
}
|
|
453
|
+
return handleReactionIntercept({
|
|
454
|
+
reaction,
|
|
438
455
|
sourceChannel,
|
|
439
456
|
sourceInterface,
|
|
440
457
|
conversationExternalId,
|
|
@@ -476,7 +493,10 @@ export async function handleChannelInbound({
|
|
|
476
493
|
// respond to one by minting a verification challenge or creating an access
|
|
477
494
|
// request (LUM-2673). Reaction callbacks never reach this point — the
|
|
478
495
|
// intercept above returns for them.
|
|
479
|
-
const isCallbackInteraction =
|
|
496
|
+
const isCallbackInteraction =
|
|
497
|
+
eventKind === "button" ||
|
|
498
|
+
eventKind === "reaction" ||
|
|
499
|
+
eventKind === "delete";
|
|
480
500
|
|
|
481
501
|
// ── Ingress ACL enforcement ──
|
|
482
502
|
const aclResult = await enforceIngressAcl({
|
|
@@ -493,26 +513,25 @@ export async function handleChannelInbound({
|
|
|
493
513
|
assistantId,
|
|
494
514
|
effectiveAdmissionPolicy: effectiveAdmissionPolicyForAcl,
|
|
495
515
|
isCallbackInteraction,
|
|
516
|
+
// Scoped to deletes: the one family whose wire can name no actor. Any
|
|
517
|
+
// other kind carrying the flag still faces the full ACL.
|
|
518
|
+
actorUnattributed:
|
|
519
|
+
eventKind === "delete" && sourceMetadata?.actorUnattributed === true,
|
|
496
520
|
});
|
|
497
521
|
if (aclResult.earlyResponse) {
|
|
498
522
|
return aclResult.earlyResponse;
|
|
499
523
|
}
|
|
500
524
|
const { resolvedMember } = aclResult;
|
|
501
525
|
|
|
502
|
-
// ──
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// the
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
// (matches the edit-intercept policy).
|
|
512
|
-
// The Slack gate mirrors the reaction intercept's: deletes are ingested
|
|
513
|
-
// from Slack alone today, and each further channel arrives as its own
|
|
514
|
-
// decision with its own wire shape.
|
|
515
|
-
if (sourceChannel === "slack" && eventKind === "delete") {
|
|
526
|
+
// ── Delete propagation ──
|
|
527
|
+
// A delete names the original via `sourceMetadata.messageId` and
|
|
528
|
+
// short-circuits the rest of the pipeline: no agent loop, no approval
|
|
529
|
+
// routing. The stored row keeps its content for audit; rendering elides on
|
|
530
|
+
// the deletedAt marker. An attributed delete (the wire names the deleted
|
|
531
|
+
// message's author, as Slack's does) passed the ingress ACL above; an
|
|
532
|
+
// unattributed one bypassed it under the ACL's stated contract and applies
|
|
533
|
+
// only to a row this daemon ingested.
|
|
534
|
+
if (eventKind === "delete") {
|
|
516
535
|
const deletedMessageTs =
|
|
517
536
|
typeof sourceMetadata?.messageId === "string"
|
|
518
537
|
? sourceMetadata.messageId
|
|
@@ -521,7 +540,7 @@ export async function handleChannelInbound({
|
|
|
521
540
|
if (!deletedMessageTs) {
|
|
522
541
|
log.debug(
|
|
523
542
|
{ conversationExternalId },
|
|
524
|
-
"
|
|
543
|
+
"Delete event missing sourceMetadata.messageId; ignoring",
|
|
525
544
|
);
|
|
526
545
|
return { accepted: true, deleted: false };
|
|
527
546
|
}
|
|
@@ -545,6 +564,20 @@ export async function handleChannelInbound({
|
|
|
545
564
|
if (original) {
|
|
546
565
|
break;
|
|
547
566
|
}
|
|
567
|
+
// The retry window exists for one race: the original's inbound-event
|
|
568
|
+
// row is written before its message link lands. A source with no row
|
|
569
|
+
// at all was never ingested, so nothing can appear by waiting, and
|
|
570
|
+
// waiting would hold this conversation's serialized lane through the
|
|
571
|
+
// full miss window for every unrelated delete a busy room produces.
|
|
572
|
+
if (
|
|
573
|
+
!hasInboundEventForSource(
|
|
574
|
+
sourceChannel,
|
|
575
|
+
conversationExternalId,
|
|
576
|
+
deletedMessageTs,
|
|
577
|
+
)
|
|
578
|
+
) {
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
548
581
|
if (attempt < deleteLookupRetries) {
|
|
549
582
|
log.info(
|
|
550
583
|
{
|
|
@@ -564,7 +597,7 @@ export async function handleChannelInbound({
|
|
|
564
597
|
if (!original) {
|
|
565
598
|
log.debug(
|
|
566
599
|
{ conversationExternalId, deletedMessageTs },
|
|
567
|
-
"No stored message found for
|
|
600
|
+
"No stored message found for delete after retries; ignoring",
|
|
568
601
|
);
|
|
569
602
|
return { accepted: true, deleted: false };
|
|
570
603
|
}
|
|
@@ -598,6 +631,9 @@ export async function handleChannelInbound({
|
|
|
598
631
|
{ deletedAt: Date.now() },
|
|
599
632
|
);
|
|
600
633
|
updateMessageMetadata(original.messageId, { providerMeta });
|
|
634
|
+
// The stamp lands in the store only; stale-marking makes a resident
|
|
635
|
+
// conversation's next turn reload and see the row as deleted.
|
|
636
|
+
findConversation(original.conversationId)?.markHistoryStale();
|
|
601
637
|
log.info(
|
|
602
638
|
{
|
|
603
639
|
conversationExternalId,
|
|
@@ -618,6 +654,7 @@ export async function handleChannelInbound({
|
|
|
618
654
|
// (channel, interface, provenance, etc.) untouched. Content column
|
|
619
655
|
// is intentionally not updated.
|
|
620
656
|
updateMessageMetadata(original.messageId, { slackMeta: updatedSlackMeta });
|
|
657
|
+
findConversation(original.conversationId)?.markHistoryStale();
|
|
621
658
|
|
|
622
659
|
log.info(
|
|
623
660
|
{
|
|
@@ -1168,11 +1205,11 @@ export async function handleChannelInbound({
|
|
|
1168
1205
|
// so checking for empty content alone would miss stale callbacks.
|
|
1169
1206
|
//
|
|
1170
1207
|
// Reaction events (`reaction:` / `reaction_removed:`) are persisted by
|
|
1171
|
-
// the earlier `
|
|
1208
|
+
// the earlier `isReactionEvent` branch and never reach here; guard
|
|
1172
1209
|
// explicitly so a future refactor can't let a reaction ts drive a
|
|
1173
1210
|
// "This approval request has been resolved." edit that would clobber
|
|
1174
1211
|
// the user's reacted-to message.
|
|
1175
|
-
if (hasCallbackData && !
|
|
1212
|
+
if (hasCallbackData && !isReactionEvent(body)) {
|
|
1176
1213
|
// Record seen signal even for stale callbacks — the user still interacted
|
|
1177
1214
|
if (sourceChannel === "telegram" || sourceChannel === "slack") {
|
|
1178
1215
|
try {
|
|
@@ -2106,6 +2143,19 @@ async function runBackfillSlackDmIfCold(params: {
|
|
|
2106
2143
|
return;
|
|
2107
2144
|
}
|
|
2108
2145
|
|
|
2146
|
+
// Guardian approval cards posted to this chat are delivery
|
|
2147
|
+
// projections of canonical requests, never conversation content, so
|
|
2148
|
+
// their message ts must not be imported as transcript rows. The
|
|
2149
|
+
// gateway delivery registry is the authority on which messages
|
|
2150
|
+
// those are (it covers cards from every daemon build). An
|
|
2151
|
+
// unreachable gateway aborts the backfill rather than importing a
|
|
2152
|
+
// card: the DM stays cold, so the next inbound message retries.
|
|
2153
|
+
const guardianCardTs = new Set(
|
|
2154
|
+
(await listGuardianRequestDeliveriesByChat("slack", params.channelId))
|
|
2155
|
+
.map((delivery) => delivery.destinationMessageId)
|
|
2156
|
+
.filter((ts): ts is string => typeof ts === "string" && ts.length > 0),
|
|
2157
|
+
);
|
|
2158
|
+
|
|
2109
2159
|
const seen = readStoredSlackChannelTs(params.conversationId);
|
|
2110
2160
|
let written = 0;
|
|
2111
2161
|
// Slack's conversation.history returns most-recent first. Reverse so
|
|
@@ -2117,6 +2167,9 @@ async function runBackfillSlackDmIfCold(params: {
|
|
|
2117
2167
|
if (seen.has(message.id)) {
|
|
2118
2168
|
continue;
|
|
2119
2169
|
}
|
|
2170
|
+
if (guardianCardTs.has(message.id)) {
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2120
2173
|
if (await isSlackAssistantThreadPlaceholder(message, params.account)) {
|
|
2121
2174
|
continue;
|
|
2122
2175
|
}
|
|
@@ -116,9 +116,9 @@ export interface AclEnforcementParams {
|
|
|
116
116
|
*/
|
|
117
117
|
effectiveAdmissionPolicy?: AdmissionPolicy;
|
|
118
118
|
/**
|
|
119
|
-
* True when the inbound event is an interaction callback (
|
|
120
|
-
*
|
|
121
|
-
*
|
|
119
|
+
* True when the inbound event is an interaction callback (a button
|
|
120
|
+
* press, a reaction, a delete: any kind that is not a composed message
|
|
121
|
+
* or an edit of one). Callbacks are decision attempts / lifecycle
|
|
122
122
|
* events, not access attempts: a denied callback must never mint a
|
|
123
123
|
* verification challenge or create an access request — a stale button
|
|
124
124
|
* press from an unrecognized sender would otherwise spawn a fresh
|
|
@@ -127,6 +127,15 @@ export interface AclEnforcementParams {
|
|
|
127
127
|
* caller makes the classification explicitly.
|
|
128
128
|
*/
|
|
129
129
|
isCallbackInteraction: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The platform named no actor for this event (the id is a channel's
|
|
132
|
+
* synthetic system identity). There is no identity claim to enforce, so
|
|
133
|
+
* the ACL neither resolves a member nor denies: the family stage that
|
|
134
|
+
* consumes such an event applies it only to rows whose author cleared
|
|
135
|
+
* this ACL when the original message arrived. Callers set this ONLY for
|
|
136
|
+
* event kinds that cannot start an agent turn.
|
|
137
|
+
*/
|
|
138
|
+
actorUnattributed?: boolean;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
/**
|
|
@@ -206,6 +215,10 @@ export async function enforceIngressAcl(
|
|
|
206
215
|
isCallbackInteraction,
|
|
207
216
|
} = params;
|
|
208
217
|
|
|
218
|
+
if (params.actorUnattributed) {
|
|
219
|
+
return { resolvedMember: null };
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
let validatedBootstrapSession: VerificationSessionWire | undefined;
|
|
210
223
|
|
|
211
224
|
// Identity signals forwarded via sourceMetadata: bot flag (Slack users.info
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* resolvable target is dropped.
|
|
17
17
|
*/
|
|
18
18
|
import type { ChannelId } from "../../../channels/types.js";
|
|
19
|
+
import { findConversation } from "../../../daemon/conversation-registry.js";
|
|
19
20
|
import {
|
|
20
21
|
mergeSlackMetadata,
|
|
21
22
|
readSlackMetadata,
|
|
@@ -227,6 +228,11 @@ export async function handleEditIntercept(
|
|
|
227
228
|
// path, so reindex the message into the lexical index — the idempotent
|
|
228
229
|
// upsert replaces the stale Qdrant point with the edited content.
|
|
229
230
|
enqueueLexicalIndexForMessage(original.messageId);
|
|
231
|
+
// The rewrite lands in the store only; a resident conversation keeps the
|
|
232
|
+
// pre-edit text in memory until it reloads, so the next turn would answer
|
|
233
|
+
// a revision the sender already replaced. Stale-marking makes that turn's
|
|
234
|
+
// history reload pick up the edit.
|
|
235
|
+
findConversation(original.conversationId)?.markHistoryStale();
|
|
230
236
|
markProcessed(editResult.eventId);
|
|
231
237
|
log.info(
|
|
232
238
|
{ assistantId, sourceMessageId, messageId: original.messageId },
|
|
@@ -9,7 +9,13 @@ let mockIdentityRequests: { id: string }[] = [];
|
|
|
9
9
|
let routeGuardianReplyCalls: unknown[] = [];
|
|
10
10
|
let deliverChannelReplyCalls: unknown[][] = [];
|
|
11
11
|
|
|
12
|
+
// Spread the actual module so transitive importers of names this
|
|
13
|
+
// factory does not stub keep resolving (a partial factory breaks at
|
|
14
|
+
// import time when the graph gains a new named import).
|
|
15
|
+
const actualGatewayGuardianRequests =
|
|
16
|
+
await import("../../../channels/gateway-guardian-requests.js");
|
|
12
17
|
mock.module("../../../channels/gateway-guardian-requests.js", () => ({
|
|
18
|
+
...actualGatewayGuardianRequests,
|
|
13
19
|
listPendingRequestsByDestinationOrEmpty: async () =>
|
|
14
20
|
mockDeliveryScopedRequests,
|
|
15
21
|
listGuardianRequestsOrEmpty: async () => mockIdentityRequests,
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Extracted from inbound-message-handler.ts to keep the top-level handler
|
|
10
10
|
* focused on orchestration.
|
|
11
11
|
*/
|
|
12
|
+
import type { InboundReactionPayload } from "@vellumai/gateway-client";
|
|
13
|
+
|
|
12
14
|
import {
|
|
13
15
|
listGuardianRequestsOrEmpty,
|
|
14
16
|
listPendingRequestsByDestinationOrEmpty,
|
|
@@ -35,6 +37,7 @@ export interface GuardianReplyInterceptParams {
|
|
|
35
37
|
trimmedContent: string;
|
|
36
38
|
hasCallbackData: boolean;
|
|
37
39
|
callbackData: string | undefined;
|
|
40
|
+
reaction?: InboundReactionPayload;
|
|
38
41
|
/**
|
|
39
42
|
* For emoji-reaction decisions: the channel-native id (Slack `ts`) of the
|
|
40
43
|
* message the reaction was attached to. Threaded to the router so it can
|
|
@@ -81,6 +84,7 @@ export async function handleGuardianReplyIntercept(
|
|
|
81
84
|
trimmedContent,
|
|
82
85
|
hasCallbackData,
|
|
83
86
|
callbackData,
|
|
87
|
+
reaction,
|
|
84
88
|
reactedMessageTs,
|
|
85
89
|
rawSenderId,
|
|
86
90
|
canonicalSenderId,
|
|
@@ -136,7 +140,7 @@ export async function handleGuardianReplyIntercept(
|
|
|
136
140
|
// Reactions address one specific request by the reacted card's message id,
|
|
137
141
|
// so they bypass the pending-request list scoping that the text/NL paths
|
|
138
142
|
// need — the router's reaction branch resolves the target directly.
|
|
139
|
-
const isReaction =
|
|
143
|
+
const isReaction = reaction !== undefined;
|
|
140
144
|
let pendingScope: GuardianPendingScope | undefined;
|
|
141
145
|
if (!isReaction) {
|
|
142
146
|
// Hint reads degrade to empty on gateway failure: Slack then blocks the
|
|
@@ -178,6 +182,7 @@ export async function handleGuardianReplyIntercept(
|
|
|
178
182
|
},
|
|
179
183
|
conversationId,
|
|
180
184
|
callbackData,
|
|
185
|
+
...(reaction ? { reaction } : {}),
|
|
181
186
|
reactedMessageTs,
|
|
182
187
|
pendingScope,
|
|
183
188
|
approvalConversationGenerator,
|