@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
|
@@ -263,19 +263,6 @@ afterAll(() => {
|
|
|
263
263
|
);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
mock.module("../memory/retriever.js", () => ({
|
|
267
|
-
buildMemoryRecall: async () => ({
|
|
268
|
-
enabled: false,
|
|
269
|
-
degraded: false,
|
|
270
|
-
injectedText: "",
|
|
271
|
-
|
|
272
|
-
semanticHits: 0,
|
|
273
|
-
injectedTokens: 0,
|
|
274
|
-
latencyMs: 0,
|
|
275
|
-
}),
|
|
276
|
-
injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
266
|
mock.module("../apps/app-store.js", () => ({
|
|
280
267
|
getApp: () => null,
|
|
281
268
|
listAppFiles: () => [],
|
|
@@ -346,6 +346,10 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
346
346
|
updateMessageMetadata: updateMessageMetadataMock,
|
|
347
347
|
setConversationHistoryStrippedAt: setConversationHistoryStrippedAtMock,
|
|
348
348
|
getMessages: () => mockStoredMessages,
|
|
349
|
+
// Read by the pre-run camera-frame retention pass for any history holding
|
|
350
|
+
// attachment references. This suite seeds none, so an empty map is the same
|
|
351
|
+
// answer the real accessor would give.
|
|
352
|
+
selectSightFrameCaptureTimes: () => new Map<string, number>(),
|
|
349
353
|
getConversation: () => mockConversationRow,
|
|
350
354
|
provenanceFromTrustContext: () => ({
|
|
351
355
|
source: "user",
|
|
@@ -421,19 +425,6 @@ mock.module("../persistence/conversation-disk-view.js", () => ({
|
|
|
421
425
|
rebuildConversationDiskViewFromDbStateMock,
|
|
422
426
|
}));
|
|
423
427
|
|
|
424
|
-
mock.module("../memory/retriever.js", () => ({
|
|
425
|
-
buildMemoryRecall: async () => ({
|
|
426
|
-
enabled: false,
|
|
427
|
-
degraded: false,
|
|
428
|
-
injectedText: "",
|
|
429
|
-
|
|
430
|
-
semanticHits: 0,
|
|
431
|
-
injectedTokens: 0,
|
|
432
|
-
latencyMs: 0,
|
|
433
|
-
}),
|
|
434
|
-
injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
|
|
435
|
-
}));
|
|
436
|
-
|
|
437
428
|
mock.module("../apps/app-store.js", () => ({
|
|
438
429
|
getApp: () => null,
|
|
439
430
|
listAppFiles: () => [],
|
|
@@ -1708,6 +1699,91 @@ describe("session-agent-loop", () => {
|
|
|
1708
1699
|
expect(call[1]).toBe(JSON.stringify(rawRequest));
|
|
1709
1700
|
expect(call[2]).toBe(JSON.stringify(rawResponse));
|
|
1710
1701
|
});
|
|
1702
|
+
|
|
1703
|
+
test("request log and usage event share the turn call site", async () => {
|
|
1704
|
+
const rawRequest = {
|
|
1705
|
+
model: "gpt-4.1",
|
|
1706
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
1707
|
+
};
|
|
1708
|
+
const rawResponse = {
|
|
1709
|
+
model: "gpt-4.1-2026-03-01",
|
|
1710
|
+
choices: [
|
|
1711
|
+
{
|
|
1712
|
+
finish_reason: "stop",
|
|
1713
|
+
message: { role: "assistant", content: "Hi there." },
|
|
1714
|
+
},
|
|
1715
|
+
],
|
|
1716
|
+
};
|
|
1717
|
+
const ctx = makeCtx({
|
|
1718
|
+
providerResponses: [
|
|
1719
|
+
{
|
|
1720
|
+
content: [{ type: "text", text: "Hi there." }],
|
|
1721
|
+
model: "gpt-4.1-2026-03-01",
|
|
1722
|
+
usage: { inputTokens: 12, outputTokens: 3 },
|
|
1723
|
+
stopReason: "end_turn",
|
|
1724
|
+
actualProvider: "openai",
|
|
1725
|
+
rawRequest,
|
|
1726
|
+
rawResponse,
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
|
|
1732
|
+
callSite: "callAgent",
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
expect(recordRequestLogMock).toHaveBeenCalledTimes(1);
|
|
1736
|
+
const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
|
|
1737
|
+
expect(call[5]).toBe("callAgent");
|
|
1738
|
+
|
|
1739
|
+
const usageCall = recordUsageMock.mock.calls.find(
|
|
1740
|
+
(entry) => (entry as unknown[])[5] === "main_agent",
|
|
1741
|
+
) as unknown[] | undefined;
|
|
1742
|
+
expect(usageCall).toBeDefined();
|
|
1743
|
+
const attribution = usageCall?.[12] as UsageAttributionInput;
|
|
1744
|
+
expect(attribution.callSite).toBe("callAgent");
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
test("stamps voiceFrontDoor on the request log for a front-door turn", async () => {
|
|
1748
|
+
const ctx = makeCtx({
|
|
1749
|
+
providerResponses: [
|
|
1750
|
+
{
|
|
1751
|
+
content: [{ type: "text", text: "Hi." }],
|
|
1752
|
+
model: "mock-model",
|
|
1753
|
+
usage: { inputTokens: 8, outputTokens: 2 },
|
|
1754
|
+
stopReason: "end_turn",
|
|
1755
|
+
rawRequest: { model: "mock-model", messages: [] },
|
|
1756
|
+
rawResponse: { id: "resp-1" },
|
|
1757
|
+
},
|
|
1758
|
+
],
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
|
|
1762
|
+
callSite: "voiceFrontDoor",
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
|
|
1766
|
+
expect(call[5]).toBe("voiceFrontDoor");
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
test("provider-error request logs carry the turn call site", async () => {
|
|
1770
|
+
const ctx = makeCtx({
|
|
1771
|
+
loopProvider: {
|
|
1772
|
+
name: "mock-provider",
|
|
1773
|
+
async sendMessage() {
|
|
1774
|
+
throw new Error("upstream 500");
|
|
1775
|
+
},
|
|
1776
|
+
} as unknown as Provider,
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
|
|
1780
|
+
callSite: "callAgent",
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
expect(recordRequestLogMock).toHaveBeenCalledTimes(1);
|
|
1784
|
+
const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
|
|
1785
|
+
expect(call[5]).toBe("callAgent");
|
|
1786
|
+
});
|
|
1711
1787
|
});
|
|
1712
1788
|
|
|
1713
1789
|
describe("usage accounting", () => {
|
|
@@ -4164,6 +4240,7 @@ describe("session-agent-loop", () => {
|
|
|
4164
4240
|
summaryModel: "claude-sonnet-5",
|
|
4165
4241
|
summaryText: "summary",
|
|
4166
4242
|
summaryCallSite: "compactionAgent",
|
|
4243
|
+
summaryResolutionCallSite: "mainAgent",
|
|
4167
4244
|
// What the compactor resolved before the call...
|
|
4168
4245
|
summaryOverrideProfile: "primaryProfile",
|
|
4169
4246
|
// ...and what the reroute actually served.
|
|
@@ -4184,6 +4261,7 @@ describe("session-agent-loop", () => {
|
|
|
4184
4261
|
expect(compactorCall?.[3]).toBe("claude-sonnet-5");
|
|
4185
4262
|
expect(compactorCall?.[12]).toEqual({
|
|
4186
4263
|
callSite: "compactionAgent",
|
|
4264
|
+
profileResolutionCallSite: "mainAgent",
|
|
4187
4265
|
overrideProfile: "backupProfile",
|
|
4188
4266
|
forceOverrideProfile: true,
|
|
4189
4267
|
});
|
|
@@ -4210,6 +4288,7 @@ describe("session-agent-loop", () => {
|
|
|
4210
4288
|
summaryModel: "mock-model",
|
|
4211
4289
|
summaryText: "summary",
|
|
4212
4290
|
summaryCallSite: "compactionAgent",
|
|
4291
|
+
summaryResolutionCallSite: "mainAgent",
|
|
4213
4292
|
summaryOverrideProfile: "primaryProfile",
|
|
4214
4293
|
summaryActualProvider: "openai",
|
|
4215
4294
|
},
|
|
@@ -4226,6 +4305,7 @@ describe("session-agent-loop", () => {
|
|
|
4226
4305
|
expect(compactorCall?.[3]).toBe("mock-model");
|
|
4227
4306
|
expect(compactorCall?.[12]).toEqual({
|
|
4228
4307
|
callSite: "compactionAgent",
|
|
4308
|
+
profileResolutionCallSite: "mainAgent",
|
|
4229
4309
|
overrideProfile: "primaryProfile",
|
|
4230
4310
|
});
|
|
4231
4311
|
});
|
|
@@ -4255,6 +4335,7 @@ describe("session-agent-loop", () => {
|
|
|
4255
4335
|
summaryModel: "mock-model",
|
|
4256
4336
|
summaryText: "summary",
|
|
4257
4337
|
summaryCallSite: "compactionAgent",
|
|
4338
|
+
summaryResolutionCallSite: "mainAgent",
|
|
4258
4339
|
summaryOverrideProfile: "primaryProfile",
|
|
4259
4340
|
},
|
|
4260
4341
|
() => {},
|
|
@@ -4271,6 +4352,7 @@ describe("session-agent-loop", () => {
|
|
|
4271
4352
|
});
|
|
4272
4353
|
expect(compactorCall?.[12]).toEqual({
|
|
4273
4354
|
callSite: "compactionAgent",
|
|
4355
|
+
profileResolutionCallSite: "mainAgent",
|
|
4274
4356
|
overrideProfile: "primaryProfile",
|
|
4275
4357
|
});
|
|
4276
4358
|
});
|
|
@@ -86,19 +86,6 @@ mock.module("../persistence/attachments-store.js", () => ({
|
|
|
86
86
|
linkAttachmentToMessage: () => {},
|
|
87
87
|
}));
|
|
88
88
|
|
|
89
|
-
mock.module("../memory/retriever.js", () => ({
|
|
90
|
-
buildMemoryRecall: async () => ({
|
|
91
|
-
enabled: false,
|
|
92
|
-
degraded: false,
|
|
93
|
-
injectedText: "",
|
|
94
|
-
|
|
95
|
-
semanticHits: 0,
|
|
96
|
-
injectedTokens: 0,
|
|
97
|
-
latencyMs: 0,
|
|
98
|
-
}),
|
|
99
|
-
injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
89
|
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
103
90
|
ContextWindowManager: class {
|
|
104
91
|
estimateInputTokens() {
|
|
@@ -435,6 +435,424 @@ describe("loadFromDb history repair", () => {
|
|
|
435
435
|
]);
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
+
test("a stored reaction row loads as rendered text, not the sentinel", async () => {
|
|
439
|
+
mockConversation = {
|
|
440
|
+
id: "conv-1",
|
|
441
|
+
contextSummary: null,
|
|
442
|
+
contextCompactedMessageCount: 0,
|
|
443
|
+
totalInputTokens: 0,
|
|
444
|
+
totalOutputTokens: 0,
|
|
445
|
+
totalEstimatedCost: 0,
|
|
446
|
+
};
|
|
447
|
+
mockDbMessages = [
|
|
448
|
+
{
|
|
449
|
+
id: "m1",
|
|
450
|
+
role: "user",
|
|
451
|
+
content: [{ type: "text", text: "Deploy is done" }],
|
|
452
|
+
metadata: JSON.stringify({
|
|
453
|
+
providerMeta: JSON.stringify({
|
|
454
|
+
source: "discord",
|
|
455
|
+
conversationExternalId: "chan-1",
|
|
456
|
+
messageId: "555.1",
|
|
457
|
+
eventKind: "message",
|
|
458
|
+
}),
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "m2",
|
|
463
|
+
role: "user",
|
|
464
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
465
|
+
metadata: JSON.stringify({
|
|
466
|
+
providerMeta: JSON.stringify({
|
|
467
|
+
source: "discord",
|
|
468
|
+
conversationExternalId: "chan-1",
|
|
469
|
+
eventKind: "reaction",
|
|
470
|
+
reaction: {
|
|
471
|
+
targetMessageId: "555.1",
|
|
472
|
+
emoji: "thumbsup",
|
|
473
|
+
op: "added",
|
|
474
|
+
actorDisplayName: "Alice",
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
477
|
+
}),
|
|
478
|
+
},
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const conversation = makeConversation();
|
|
482
|
+
await conversation.loadFromDb();
|
|
483
|
+
const messages = conversation.getMessages();
|
|
484
|
+
|
|
485
|
+
// History repair merges the two adjacent user rows into one message;
|
|
486
|
+
// what matters is that the model reads the rendered fact, never the
|
|
487
|
+
// sentinel.
|
|
488
|
+
const allText = messages
|
|
489
|
+
.flatMap((m) => m.content)
|
|
490
|
+
.filter((b) => b.type === "text")
|
|
491
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
492
|
+
.join("\n");
|
|
493
|
+
expect(allText).not.toContain("[reaction]");
|
|
494
|
+
expect(allText).toContain(
|
|
495
|
+
'Alice reacted with :thumbsup: to the message "Deploy is done"',
|
|
496
|
+
);
|
|
497
|
+
expect(allText).toContain("<external_content");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("markHistoryStale makes the next ensureActorScopedHistory reload", async () => {
|
|
501
|
+
mockConversation = {
|
|
502
|
+
id: "conv-1",
|
|
503
|
+
contextSummary: null,
|
|
504
|
+
contextCompactedMessageCount: 0,
|
|
505
|
+
totalInputTokens: 0,
|
|
506
|
+
totalOutputTokens: 0,
|
|
507
|
+
totalEstimatedCost: 0,
|
|
508
|
+
};
|
|
509
|
+
mockDbMessages = [
|
|
510
|
+
{
|
|
511
|
+
id: "m1",
|
|
512
|
+
role: "user",
|
|
513
|
+
content: [{ type: "text", text: "Hello" }],
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "m2",
|
|
517
|
+
role: "assistant",
|
|
518
|
+
content: [{ type: "text", text: "Hi" }],
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
|
|
522
|
+
const conversation = makeConversation();
|
|
523
|
+
conversation.setTrustContext({
|
|
524
|
+
trustClass: "guardian",
|
|
525
|
+
sourceChannel: "telegram",
|
|
526
|
+
});
|
|
527
|
+
await conversation.loadFromDb();
|
|
528
|
+
expect(conversation.getMessages()).toHaveLength(2);
|
|
529
|
+
|
|
530
|
+
// A channel event lands straight in the store while the conversation
|
|
531
|
+
// stays resident.
|
|
532
|
+
mockDbMessages.push({
|
|
533
|
+
id: "m3",
|
|
534
|
+
role: "user",
|
|
535
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Same actor, no stale mark: the resident history is reused.
|
|
539
|
+
await conversation.ensureActorScopedHistory();
|
|
540
|
+
expect(conversation.getMessages()).toHaveLength(2);
|
|
541
|
+
|
|
542
|
+
conversation.markHistoryStale();
|
|
543
|
+
await conversation.ensureActorScopedHistory();
|
|
544
|
+
expect(conversation.getMessages()).toHaveLength(3);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("a deleted row loads as the deletion marker, not its original text", async () => {
|
|
548
|
+
mockConversation = {
|
|
549
|
+
id: "conv-1",
|
|
550
|
+
contextSummary: null,
|
|
551
|
+
contextCompactedMessageCount: 0,
|
|
552
|
+
totalInputTokens: 0,
|
|
553
|
+
totalOutputTokens: 0,
|
|
554
|
+
totalEstimatedCost: 0,
|
|
555
|
+
};
|
|
556
|
+
mockDbMessages = [
|
|
557
|
+
{
|
|
558
|
+
id: "m1",
|
|
559
|
+
role: "user",
|
|
560
|
+
content: [
|
|
561
|
+
{ type: "text", text: "Please forget this password: hunter2" },
|
|
562
|
+
],
|
|
563
|
+
metadata: JSON.stringify({
|
|
564
|
+
providerMeta: JSON.stringify({
|
|
565
|
+
source: "discord",
|
|
566
|
+
conversationExternalId: "chan-1",
|
|
567
|
+
messageId: "555.1",
|
|
568
|
+
eventKind: "message",
|
|
569
|
+
deletedAt: 1700000001000,
|
|
570
|
+
}),
|
|
571
|
+
}),
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: "m2",
|
|
575
|
+
role: "assistant",
|
|
576
|
+
content: [{ type: "text", text: "Understood" }],
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
const conversation = makeConversation();
|
|
581
|
+
await conversation.loadFromDb();
|
|
582
|
+
const allText = conversation
|
|
583
|
+
.getMessages()
|
|
584
|
+
.flatMap((m) => m.content)
|
|
585
|
+
.filter((b) => b.type === "text")
|
|
586
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
587
|
+
.join("\n");
|
|
588
|
+
expect(allText).toContain("[This message was deleted]");
|
|
589
|
+
expect(allText).not.toContain("hunter2");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("a reaction never quotes a target that was later deleted", async () => {
|
|
593
|
+
mockConversation = {
|
|
594
|
+
id: "conv-1",
|
|
595
|
+
contextSummary: null,
|
|
596
|
+
contextCompactedMessageCount: 0,
|
|
597
|
+
totalInputTokens: 0,
|
|
598
|
+
totalOutputTokens: 0,
|
|
599
|
+
totalEstimatedCost: 0,
|
|
600
|
+
};
|
|
601
|
+
mockDbMessages = [
|
|
602
|
+
{
|
|
603
|
+
id: "m1",
|
|
604
|
+
role: "user",
|
|
605
|
+
content: [{ type: "text", text: "secret text" }],
|
|
606
|
+
metadata: JSON.stringify({
|
|
607
|
+
providerMeta: JSON.stringify({
|
|
608
|
+
source: "discord",
|
|
609
|
+
conversationExternalId: "chan-1",
|
|
610
|
+
messageId: "555.1",
|
|
611
|
+
eventKind: "message",
|
|
612
|
+
deletedAt: 1700000001000,
|
|
613
|
+
}),
|
|
614
|
+
}),
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
id: "m2",
|
|
618
|
+
role: "user",
|
|
619
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
620
|
+
metadata: JSON.stringify({
|
|
621
|
+
providerMeta: JSON.stringify({
|
|
622
|
+
source: "discord",
|
|
623
|
+
conversationExternalId: "chan-1",
|
|
624
|
+
eventKind: "reaction",
|
|
625
|
+
reaction: {
|
|
626
|
+
targetMessageId: "555.1",
|
|
627
|
+
emoji: "thumbsup",
|
|
628
|
+
op: "added",
|
|
629
|
+
actorDisplayName: "Alice",
|
|
630
|
+
},
|
|
631
|
+
}),
|
|
632
|
+
}),
|
|
633
|
+
},
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
const conversation = makeConversation();
|
|
637
|
+
await conversation.loadFromDb();
|
|
638
|
+
const allText = conversation
|
|
639
|
+
.getMessages()
|
|
640
|
+
.flatMap((m) => m.content)
|
|
641
|
+
.filter((b) => b.type === "text")
|
|
642
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
643
|
+
.join("\n");
|
|
644
|
+
expect(allText).toContain("reacted with :thumbsup: to an earlier message");
|
|
645
|
+
expect(allText).not.toContain("secret text");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("a reaction naming a later post of a split reply still quotes it", async () => {
|
|
649
|
+
mockConversation = {
|
|
650
|
+
id: "conv-1",
|
|
651
|
+
contextSummary: null,
|
|
652
|
+
contextCompactedMessageCount: 0,
|
|
653
|
+
totalInputTokens: 0,
|
|
654
|
+
totalOutputTokens: 0,
|
|
655
|
+
totalEstimatedCost: 0,
|
|
656
|
+
};
|
|
657
|
+
mockDbMessages = [
|
|
658
|
+
{
|
|
659
|
+
id: "m1",
|
|
660
|
+
role: "assistant",
|
|
661
|
+
content: [{ type: "text", text: "Deploy finished cleanly." }],
|
|
662
|
+
metadata: JSON.stringify({
|
|
663
|
+
providerMeta: JSON.stringify({
|
|
664
|
+
source: "discord",
|
|
665
|
+
conversationExternalId: "chan-1",
|
|
666
|
+
messageId: "555.1",
|
|
667
|
+
additionalMessageIds: ["555.2"],
|
|
668
|
+
eventKind: "message",
|
|
669
|
+
}),
|
|
670
|
+
}),
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
id: "m2",
|
|
674
|
+
role: "user",
|
|
675
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
676
|
+
metadata: JSON.stringify({
|
|
677
|
+
providerMeta: JSON.stringify({
|
|
678
|
+
source: "discord",
|
|
679
|
+
conversationExternalId: "chan-1",
|
|
680
|
+
eventKind: "reaction",
|
|
681
|
+
reaction: {
|
|
682
|
+
targetMessageId: "555.2",
|
|
683
|
+
emoji: "tada",
|
|
684
|
+
op: "added",
|
|
685
|
+
actorDisplayName: "Alice",
|
|
686
|
+
},
|
|
687
|
+
}),
|
|
688
|
+
}),
|
|
689
|
+
},
|
|
690
|
+
];
|
|
691
|
+
|
|
692
|
+
const conversation = makeConversation();
|
|
693
|
+
await conversation.loadFromDb();
|
|
694
|
+
const allText = conversation
|
|
695
|
+
.getMessages()
|
|
696
|
+
.flatMap((m) => m.content)
|
|
697
|
+
.filter((b) => b.type === "text")
|
|
698
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
699
|
+
.join("\n");
|
|
700
|
+
expect(allText).toContain(":tada:");
|
|
701
|
+
// The quote resolves through the split reply's later post id: the line
|
|
702
|
+
// never degrades to the unresolved "an earlier message" form.
|
|
703
|
+
expect(allText).not.toContain("an earlier message");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("the assistant's own reaction row renders second-person", async () => {
|
|
707
|
+
mockConversation = {
|
|
708
|
+
id: "conv-1",
|
|
709
|
+
contextSummary: null,
|
|
710
|
+
contextCompactedMessageCount: 0,
|
|
711
|
+
totalInputTokens: 0,
|
|
712
|
+
totalOutputTokens: 0,
|
|
713
|
+
totalEstimatedCost: 0,
|
|
714
|
+
};
|
|
715
|
+
mockDbMessages = [
|
|
716
|
+
{
|
|
717
|
+
id: "m1",
|
|
718
|
+
role: "user",
|
|
719
|
+
content: [{ type: "text", text: "great work" }],
|
|
720
|
+
metadata: JSON.stringify({
|
|
721
|
+
providerMeta: JSON.stringify({
|
|
722
|
+
source: "discord",
|
|
723
|
+
conversationExternalId: "chan-1",
|
|
724
|
+
messageId: "555.9",
|
|
725
|
+
eventKind: "message",
|
|
726
|
+
}),
|
|
727
|
+
}),
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
id: "m2",
|
|
731
|
+
role: "assistant",
|
|
732
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
733
|
+
metadata: JSON.stringify({
|
|
734
|
+
messageKind: "reaction",
|
|
735
|
+
providerMeta: JSON.stringify({
|
|
736
|
+
source: "discord",
|
|
737
|
+
conversationExternalId: "chan-1",
|
|
738
|
+
eventKind: "reaction",
|
|
739
|
+
reaction: {
|
|
740
|
+
targetMessageId: "555.9",
|
|
741
|
+
emoji: "🎉",
|
|
742
|
+
op: "added",
|
|
743
|
+
},
|
|
744
|
+
}),
|
|
745
|
+
}),
|
|
746
|
+
},
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
const conversation = makeConversation();
|
|
750
|
+
await conversation.loadFromDb();
|
|
751
|
+
const allText = conversation
|
|
752
|
+
.getMessages()
|
|
753
|
+
.flatMap((m) => m.content)
|
|
754
|
+
.filter((b) => b.type === "text")
|
|
755
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
756
|
+
.join("\n");
|
|
757
|
+
expect(allText).not.toContain("[reaction]");
|
|
758
|
+
expect(allText).toContain("You reacted with 🎉 to this message:");
|
|
759
|
+
// The quoted target is sender text and stays fenced even in the
|
|
760
|
+
// assistant's own row.
|
|
761
|
+
expect(allText).toContain("great work");
|
|
762
|
+
expect(allText).toContain("<external_content");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("a reaction row does not count as a turn on rehydration", async () => {
|
|
766
|
+
mockConversation = {
|
|
767
|
+
id: "conv-1",
|
|
768
|
+
contextSummary: null,
|
|
769
|
+
contextCompactedMessageCount: 0,
|
|
770
|
+
totalInputTokens: 0,
|
|
771
|
+
totalOutputTokens: 0,
|
|
772
|
+
totalEstimatedCost: 0,
|
|
773
|
+
};
|
|
774
|
+
mockDbMessages = [
|
|
775
|
+
{
|
|
776
|
+
id: "m1",
|
|
777
|
+
role: "user",
|
|
778
|
+
content: [{ type: "text", text: "Hello" }],
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
id: "m2",
|
|
782
|
+
role: "assistant",
|
|
783
|
+
content: [{ type: "text", text: "Hi" }],
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
id: "m3",
|
|
787
|
+
role: "user",
|
|
788
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
789
|
+
metadata: JSON.stringify({
|
|
790
|
+
providerMeta: JSON.stringify({
|
|
791
|
+
source: "discord",
|
|
792
|
+
conversationExternalId: "chan-1",
|
|
793
|
+
eventKind: "reaction",
|
|
794
|
+
reaction: {
|
|
795
|
+
targetMessageId: "555.1",
|
|
796
|
+
emoji: "thumbsup",
|
|
797
|
+
op: "added",
|
|
798
|
+
},
|
|
799
|
+
}),
|
|
800
|
+
}),
|
|
801
|
+
},
|
|
802
|
+
];
|
|
803
|
+
|
|
804
|
+
const conversation = makeConversation();
|
|
805
|
+
await conversation.loadFromDb();
|
|
806
|
+
// One real user message; the reaction row never dispatched a turn, so
|
|
807
|
+
// the rehydrated counter must match the live counter's one increment.
|
|
808
|
+
expect(conversation.turnCount).toBe(1);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test("a slackMeta-shaped reaction row renders with its actor id as origin", async () => {
|
|
812
|
+
mockConversation = {
|
|
813
|
+
id: "conv-1",
|
|
814
|
+
contextSummary: null,
|
|
815
|
+
contextCompactedMessageCount: 0,
|
|
816
|
+
totalInputTokens: 0,
|
|
817
|
+
totalOutputTokens: 0,
|
|
818
|
+
totalEstimatedCost: 0,
|
|
819
|
+
};
|
|
820
|
+
mockDbMessages = [
|
|
821
|
+
{
|
|
822
|
+
id: "m1",
|
|
823
|
+
role: "user",
|
|
824
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
825
|
+
metadata: JSON.stringify({
|
|
826
|
+
slackMeta: JSON.stringify({
|
|
827
|
+
source: "slack",
|
|
828
|
+
channelId: "C123",
|
|
829
|
+
channelTs: "1700000000.111111",
|
|
830
|
+
eventKind: "reaction",
|
|
831
|
+
actorExternalUserId: "U_REACTOR",
|
|
832
|
+
displayName: "Alice",
|
|
833
|
+
reaction: {
|
|
834
|
+
emoji: "tada",
|
|
835
|
+
targetChannelTs: "1700000000.111111",
|
|
836
|
+
op: "added",
|
|
837
|
+
actorDisplayName: "Alice",
|
|
838
|
+
},
|
|
839
|
+
}),
|
|
840
|
+
}),
|
|
841
|
+
},
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
const conversation = makeConversation();
|
|
845
|
+
await conversation.loadFromDb();
|
|
846
|
+
const allText = conversation
|
|
847
|
+
.getMessages()
|
|
848
|
+
.flatMap((m) => m.content)
|
|
849
|
+
.filter((b) => b.type === "text")
|
|
850
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
851
|
+
.join("\n");
|
|
852
|
+
expect(allText).toContain("Alice reacted with :tada:");
|
|
853
|
+
expect(allText).toContain('origin="U_REACTOR"');
|
|
854
|
+
});
|
|
855
|
+
|
|
438
856
|
test("persistUserMessage reloads actor-scoped history before persisting on role switch", async () => {
|
|
439
857
|
mockConversation = {
|
|
440
858
|
id: "conv-1",
|
|
@@ -68,9 +68,40 @@ describe("stripMediaPayloadsForRetry", () => {
|
|
|
68
68
|
// The image in the first message should be replaced with a text stub
|
|
69
69
|
const firstMsg = result.messages[0];
|
|
70
70
|
expect(firstMsg.content[1].type).toBe("text");
|
|
71
|
+
// Pinned whole, not just the prefix: the descriptor half comes from the
|
|
72
|
+
// shared `mediaSourceDescriptor`, and this is what keeps this stub and the
|
|
73
|
+
// camera-frame stub describing dropped media the same way. `makeImageBlock`
|
|
74
|
+
// carries 4 base64 chars, which decode to 3 bytes.
|
|
75
|
+
expect((firstMsg.content[1] as { type: "text"; text: string }).text).toBe(
|
|
76
|
+
"[Image omitted from retry context: image/png, 3 bytes]",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a stripped file block reports the same descriptor as an image", () => {
|
|
81
|
+
const fileBlock: ContentBlock = {
|
|
82
|
+
type: "file",
|
|
83
|
+
source: {
|
|
84
|
+
type: "workspace_ref",
|
|
85
|
+
media_type: "application/pdf",
|
|
86
|
+
attachmentId: "att-1",
|
|
87
|
+
sizeBytes: 2048,
|
|
88
|
+
filename: "notes.pdf",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const messages: Message[] = [
|
|
92
|
+
makeUserMessage({ type: "text", text: "old" }, fileBlock),
|
|
93
|
+
makeAssistantMessage({ type: "text", text: "response" }),
|
|
94
|
+
makeUserMessage({ type: "text", text: "new" }),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const result = stripMediaPayloadsForRetry(messages);
|
|
98
|
+
|
|
99
|
+
expect(result.replacedBlocks).toBe(1);
|
|
71
100
|
expect(
|
|
72
|
-
(
|
|
73
|
-
).
|
|
101
|
+
(result.messages[0].content[1] as { type: "text"; text: string }).text,
|
|
102
|
+
).toBe(
|
|
103
|
+
"[File omitted from retry context: notes.pdf (application/pdf, 2048 bytes)]",
|
|
104
|
+
);
|
|
74
105
|
});
|
|
75
106
|
|
|
76
107
|
test("strips images from older user turns, keeps images in latest kept turn", () => {
|