@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
|
@@ -79,10 +79,7 @@ import { initializeDb } from "../persistence/db-init.js";
|
|
|
79
79
|
import { linkMessage, recordInbound } from "../persistence/delivery-crud.js";
|
|
80
80
|
import { messages } from "../persistence/schema/conversations.js";
|
|
81
81
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
82
|
-
import {
|
|
83
|
-
isSlackReactionEvent,
|
|
84
|
-
parseSlackReactionCallbackData,
|
|
85
|
-
} from "../runtime/routes/inbound-stages/reaction-intercept.js";
|
|
82
|
+
import { isReactionEvent } from "../runtime/routes/inbound-stages/reaction-intercept.js";
|
|
86
83
|
import {
|
|
87
84
|
handleChannelInbound,
|
|
88
85
|
seedContactChannel,
|
|
@@ -109,6 +106,7 @@ function resetState(): void {
|
|
|
109
106
|
db.run("DELETE FROM contact_channels");
|
|
110
107
|
db.run("DELETE FROM contacts");
|
|
111
108
|
gatewayGuardians = [];
|
|
109
|
+
_conversationMocks.clear();
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
function seedActiveMember(): void {
|
|
@@ -202,70 +200,26 @@ function readPersistedMessages(): Array<{
|
|
|
202
200
|
// Helper unit tests
|
|
203
201
|
// ---------------------------------------------------------------------------
|
|
204
202
|
|
|
205
|
-
describe("
|
|
206
|
-
test("returns true for reaction
|
|
207
|
-
expect(
|
|
208
|
-
isSlackReactionEvent({
|
|
209
|
-
sourceChannel: "slack",
|
|
210
|
-
callbackData: "reaction:thumbsup",
|
|
211
|
-
}),
|
|
212
|
-
).toBe(true);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("returns true for reaction removed", () => {
|
|
216
|
-
expect(
|
|
217
|
-
isSlackReactionEvent({
|
|
218
|
-
sourceChannel: "slack",
|
|
219
|
-
callbackData: "reaction_removed:eyes",
|
|
220
|
-
}),
|
|
221
|
-
).toBe(true);
|
|
203
|
+
describe("isReactionEvent", () => {
|
|
204
|
+
test("returns true for a stamped reaction kind", () => {
|
|
205
|
+
expect(isReactionEvent({ eventKind: "reaction" })).toBe(true);
|
|
222
206
|
});
|
|
223
207
|
|
|
224
|
-
test("returns
|
|
225
|
-
expect(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}),
|
|
230
|
-
).toBe(false);
|
|
208
|
+
test("returns true for replayed reaction sentinels, added and removed", () => {
|
|
209
|
+
expect(isReactionEvent({ callbackData: "reaction:thumbsup" })).toBe(true);
|
|
210
|
+
expect(isReactionEvent({ callbackData: "reaction_removed:eyes" })).toBe(
|
|
211
|
+
true,
|
|
212
|
+
);
|
|
231
213
|
});
|
|
232
214
|
|
|
233
215
|
test("returns false for non-reaction callback data", () => {
|
|
234
|
-
expect(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
callbackData: "apr:req-1:approve_once",
|
|
238
|
-
}),
|
|
239
|
-
).toBe(false);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("returns false when callbackData missing", () => {
|
|
243
|
-
expect(isSlackReactionEvent({ sourceChannel: "slack" })).toBe(false);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
describe("parseSlackReactionCallbackData", () => {
|
|
248
|
-
test("parses reaction:<emoji> as added", () => {
|
|
249
|
-
expect(parseSlackReactionCallbackData("reaction:thumbsup")).toEqual({
|
|
250
|
-
op: "added",
|
|
251
|
-
emoji: "thumbsup",
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test("parses reaction_removed:<emoji> as removed", () => {
|
|
256
|
-
expect(parseSlackReactionCallbackData("reaction_removed:eyes")).toEqual({
|
|
257
|
-
op: "removed",
|
|
258
|
-
emoji: "eyes",
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("returns null for empty emoji portion", () => {
|
|
263
|
-
expect(parseSlackReactionCallbackData("reaction:")).toBeNull();
|
|
264
|
-
expect(parseSlackReactionCallbackData("reaction_removed:")).toBeNull();
|
|
216
|
+
expect(isReactionEvent({ callbackData: "apr:req-1:approve_once" })).toBe(
|
|
217
|
+
false,
|
|
218
|
+
);
|
|
265
219
|
});
|
|
266
220
|
|
|
267
|
-
test("returns
|
|
268
|
-
expect(
|
|
221
|
+
test("returns false for a plain message", () => {
|
|
222
|
+
expect(isReactionEvent({})).toBe(false);
|
|
269
223
|
});
|
|
270
224
|
});
|
|
271
225
|
|
|
@@ -311,6 +265,10 @@ describe("Slack reaction event persistence", () => {
|
|
|
311
265
|
expect(row.content).toBe("[reaction]");
|
|
312
266
|
|
|
313
267
|
const envelope = JSON.parse(row.metadata!) as Record<string, unknown>;
|
|
268
|
+
// Provenance keeps the row visible to actor-scoped history loads:
|
|
269
|
+
// filterMessagesForUntrustedActor drops rows with no trust class.
|
|
270
|
+
expect(envelope.provenanceTrustClass).toBe("trusted_contact");
|
|
271
|
+
expect(envelope.provenanceSourceChannel).toBe("slack");
|
|
314
272
|
const slackMetaRaw = envelope.slackMeta;
|
|
315
273
|
expect(typeof slackMetaRaw).toBe("string");
|
|
316
274
|
|
|
@@ -323,6 +281,9 @@ describe("Slack reaction event persistence", () => {
|
|
|
323
281
|
// Slack sends no thread on a reaction, so the row claims none.
|
|
324
282
|
expect(slackMeta!.threadTs).toBeUndefined();
|
|
325
283
|
expect(slackMeta!.displayName).toBe(SLACK_DISPLAY_NAME);
|
|
284
|
+
// Stable identity, not the sender-controlled label: the history
|
|
285
|
+
// renderer attributes the fenced line's origin by this id.
|
|
286
|
+
expect(slackMeta!.actorExternalUserId).toBe(SLACK_USER_ID);
|
|
326
287
|
expect(slackMeta!.reaction).toEqual({
|
|
327
288
|
emoji: "thumbsup",
|
|
328
289
|
actorDisplayName: SLACK_DISPLAY_NAME,
|
|
@@ -356,8 +317,10 @@ describe("Slack reaction event persistence", () => {
|
|
|
356
317
|
});
|
|
357
318
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
358
319
|
expect(resp.status).toBe(200);
|
|
320
|
+
// The payload resolver requires a target message id, so the dispatch
|
|
321
|
+
// drops the event before the intercept and it never reads as a message.
|
|
359
322
|
expect(((await resp.json()) as Record<string, unknown>).reaction).toBe(
|
|
360
|
-
"
|
|
323
|
+
"dropped_unresolvable_payload",
|
|
361
324
|
);
|
|
362
325
|
|
|
363
326
|
const rows = readPersistedMessages();
|
|
@@ -459,6 +422,39 @@ describe("Slack reaction event persistence", () => {
|
|
|
459
422
|
expect(rows.length).toBe(1);
|
|
460
423
|
});
|
|
461
424
|
|
|
425
|
+
test("a persisted reaction stale-marks the resident conversation", async () => {
|
|
426
|
+
const conversationId = seedStoredMessage("1700000000.111111");
|
|
427
|
+
const markHistoryStale = mock(() => {});
|
|
428
|
+
_conversationMocks.set(conversationId, {
|
|
429
|
+
markHistoryStale,
|
|
430
|
+
} as unknown as Conversation);
|
|
431
|
+
|
|
432
|
+
const resp = await handleChannelInbound(
|
|
433
|
+
buildReactionRequest("reaction:thumbsup"),
|
|
434
|
+
undefined,
|
|
435
|
+
TEST_BEARER_TOKEN,
|
|
436
|
+
);
|
|
437
|
+
expect(resp.status).toBe(200);
|
|
438
|
+
expect(markHistoryStale).toHaveBeenCalledTimes(1);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("a duplicate reaction does not stale-mark again", async () => {
|
|
442
|
+
const conversationId = seedStoredMessage("1700000000.111111");
|
|
443
|
+
const markHistoryStale = mock(() => {});
|
|
444
|
+
_conversationMocks.set(conversationId, {
|
|
445
|
+
markHistoryStale,
|
|
446
|
+
} as unknown as Conversation);
|
|
447
|
+
const sharedExternalMessageId = `${SLACK_CHANNEL_ID}:1700000000.777777:bob`;
|
|
448
|
+
const makeReq = () =>
|
|
449
|
+
buildReactionRequest("reaction:tada", {
|
|
450
|
+
externalMessageId: sharedExternalMessageId,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await handleChannelInbound(makeReq(), undefined, TEST_BEARER_TOKEN);
|
|
454
|
+
await handleChannelInbound(makeReq(), undefined, TEST_BEARER_TOKEN);
|
|
455
|
+
expect(markHistoryStale).toHaveBeenCalledTimes(1);
|
|
456
|
+
});
|
|
457
|
+
|
|
462
458
|
test("reaction on the assistant's own post lands in that conversation", async () => {
|
|
463
459
|
// An outbound post opens no inbound event, so the only record of its ts
|
|
464
460
|
// is the `slackMeta` on the assistant row. Seeded here the way the
|
|
@@ -148,6 +148,73 @@ describe("retry normalization: adaptive-only thinking models", () => {
|
|
|
148
148
|
expect(lastConfig()?.thinking).toEqual({ type: "adaptive" });
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test("preserves effort when dropping stale disabled thinking for Fireworks GLM 5.3", async () => {
|
|
152
|
+
// GIVEN a profile that kept `thinking: { enabled: false }` after switching
|
|
153
|
+
// to an always-on GLM 5.3 model, plus an explicit effort
|
|
154
|
+
setLlmConfig({
|
|
155
|
+
callSites: {
|
|
156
|
+
memoryExtraction: {
|
|
157
|
+
provider: "fireworks",
|
|
158
|
+
model: "accounts/fireworks/models/glm-5p3",
|
|
159
|
+
thinking: { enabled: false },
|
|
160
|
+
effort: "high",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const { provider, lastConfig } = makePipeline("fireworks");
|
|
165
|
+
|
|
166
|
+
// WHEN a request resolves through the call-site config
|
|
167
|
+
await provider.sendMessage([userMessage], {
|
|
168
|
+
config: { callSite: "memoryExtraction" },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// THEN the disabled thinking is dropped BEFORE it can be encoded as
|
|
172
|
+
// `effort: "none"` (which GLM 5.3 rejects), and the chosen effort survives
|
|
173
|
+
expect(lastConfig()?.thinking).toBeUndefined();
|
|
174
|
+
expect(lastConfig()?.effort).toBe("high");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("does not generate the opt-out effort for GLM 5.3 Flash with disabled thinking", async () => {
|
|
178
|
+
// GIVEN a pass-through caller with the wire-shape disabled config and no
|
|
179
|
+
// explicit effort
|
|
180
|
+
const { provider, lastConfig } = makePipeline("fireworks");
|
|
181
|
+
|
|
182
|
+
// WHEN sending against the always-on Flash model
|
|
183
|
+
await provider.sendMessage([userMessage], {
|
|
184
|
+
config: {
|
|
185
|
+
model: "accounts/fireworks/models/glm-5p3-flash",
|
|
186
|
+
thinking: { type: "disabled" },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// THEN no `effort: "none"` is synthesized: the model runs at its own
|
|
191
|
+
// default instead of tripping the reasoning opt-out rejection retry
|
|
192
|
+
expect(lastConfig()?.thinking).toBeUndefined();
|
|
193
|
+
expect(lastConfig()?.effort).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("still encodes the opt-out for Fireworks models that support disabling thinking", async () => {
|
|
197
|
+
// GIVEN a disabled thinking config on GLM 5.2, which is not adaptive-only
|
|
198
|
+
setLlmConfig({
|
|
199
|
+
callSites: {
|
|
200
|
+
memoryExtraction: {
|
|
201
|
+
provider: "fireworks",
|
|
202
|
+
model: "accounts/fireworks/models/glm-5p2",
|
|
203
|
+
thinking: { enabled: false },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const { provider, lastConfig } = makePipeline("fireworks");
|
|
208
|
+
|
|
209
|
+
// WHEN a request resolves through the call-site config
|
|
210
|
+
await provider.sendMessage([userMessage], {
|
|
211
|
+
config: { callSite: "memoryExtraction" },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// THEN the opt-out is still encoded through the effort knob
|
|
215
|
+
expect(lastConfig()?.effort).toBe("none");
|
|
216
|
+
});
|
|
217
|
+
|
|
151
218
|
test("preserves disabled thinking for models that support disabling it", async () => {
|
|
152
219
|
// GIVEN a call-site config disabling thinking for a non-adaptive-only model
|
|
153
220
|
setLlmConfig({
|
|
@@ -161,6 +161,28 @@ describe("renderHistoryContent", () => {
|
|
|
161
161
|
expect(output.textSegments[1]).toContain("[File attachment] dream-015.md");
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
test("an image block's _attachmentId contributes no attachment ref", () => {
|
|
165
|
+
// Chip positioning joins DB attachment rows against the refs collected
|
|
166
|
+
// here, and only `file` blocks are collected. An image carrying the same
|
|
167
|
+
// internal id must stay out of that join, or a user upload would gain a
|
|
168
|
+
// second, duplicate chip.
|
|
169
|
+
const output = renderHistoryContent([
|
|
170
|
+
{ type: "text", text: "what is this" },
|
|
171
|
+
{
|
|
172
|
+
type: "image",
|
|
173
|
+
source: {
|
|
174
|
+
type: "base64",
|
|
175
|
+
media_type: "image/jpeg",
|
|
176
|
+
data: Buffer.from("img").toString("base64"),
|
|
177
|
+
},
|
|
178
|
+
_attachmentId: "att-image-1",
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
expect(output.attachments).toEqual([]);
|
|
183
|
+
expect(output.contentOrder).toEqual(["text:0"]);
|
|
184
|
+
});
|
|
185
|
+
|
|
164
186
|
test("omits attachmentId when file block has no _attachmentId", () => {
|
|
165
187
|
const output = renderHistoryContent([
|
|
166
188
|
{
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
KEEP_LATEST_SIGHT_FRAMES,
|
|
5
|
+
MAX_SIGHT_KEEP_LATEST_FRAMES,
|
|
6
|
+
} from "../config/schemas/sight.js";
|
|
7
|
+
import { applySightFrameRetention } from "../daemon/conversation-runtime-assembly.js";
|
|
8
|
+
import {
|
|
9
|
+
getAttachmentMetadataForMessage,
|
|
10
|
+
linkAttachmentToMessage,
|
|
11
|
+
uploadAttachment,
|
|
12
|
+
} from "../persistence/attachments-store.js";
|
|
13
|
+
import {
|
|
14
|
+
addMessage,
|
|
15
|
+
createConversation,
|
|
16
|
+
forkConversation,
|
|
17
|
+
getMessages,
|
|
18
|
+
selectSightFrameCaptureTimes,
|
|
19
|
+
} from "../persistence/conversation-crud.js";
|
|
20
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
21
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
22
|
+
import { conversations } from "../persistence/schema.js";
|
|
23
|
+
import type { ContentBlock, Message } from "../providers/types.js";
|
|
24
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
25
|
+
|
|
26
|
+
await initializeDb();
|
|
27
|
+
|
|
28
|
+
function ensureConversation(id: string): void {
|
|
29
|
+
const db = getDb();
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
db.insert(conversations)
|
|
32
|
+
.values({
|
|
33
|
+
id,
|
|
34
|
+
title: "test",
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
lastMessageAt: now,
|
|
38
|
+
})
|
|
39
|
+
.onConflictDoNothing()
|
|
40
|
+
.run();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function frameContent(attachmentId: string): string {
|
|
44
|
+
return JSON.stringify([
|
|
45
|
+
{ type: "text", text: "what am I looking at" },
|
|
46
|
+
{
|
|
47
|
+
type: "image",
|
|
48
|
+
source: {
|
|
49
|
+
type: "workspace_ref",
|
|
50
|
+
media_type: "image/jpeg",
|
|
51
|
+
attachmentId,
|
|
52
|
+
sizeBytes: 1024,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function referenceImage(
|
|
59
|
+
attachmentId: string,
|
|
60
|
+
): Extract<ContentBlock, { type: "image" }> {
|
|
61
|
+
return {
|
|
62
|
+
type: "image",
|
|
63
|
+
source: {
|
|
64
|
+
type: "workspace_ref",
|
|
65
|
+
media_type: "image/jpeg",
|
|
66
|
+
attachmentId,
|
|
67
|
+
sizeBytes: 1024,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("selectSightFrameCaptureTimes", () => {
|
|
73
|
+
const conversationId = "conv-sight-frame-times";
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
ensureConversation(conversationId);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("maps tagged attachment ids to the row that carried them", async () => {
|
|
80
|
+
const first = await addMessage(
|
|
81
|
+
conversationId,
|
|
82
|
+
"user",
|
|
83
|
+
frameContent("att-1"),
|
|
84
|
+
{ metadata: { sightFrameAttachmentIds: ["att-1"] } },
|
|
85
|
+
);
|
|
86
|
+
const second = await addMessage(
|
|
87
|
+
conversationId,
|
|
88
|
+
"user",
|
|
89
|
+
frameContent("att-2"),
|
|
90
|
+
{ metadata: { sightFrameAttachmentIds: ["att-2", "att-3"] } },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const rows = getMessages(conversationId);
|
|
94
|
+
const createdAtById = new Map(rows.map((r) => [r.id, r.createdAt]));
|
|
95
|
+
const captureTimes = selectSightFrameCaptureTimes(conversationId);
|
|
96
|
+
|
|
97
|
+
expect(captureTimes.get("att-1")).toBe(createdAtById.get(first.id)!);
|
|
98
|
+
expect(captureTimes.get("att-2")).toBe(createdAtById.get(second.id)!);
|
|
99
|
+
expect(captureTimes.get("att-3")).toBe(createdAtById.get(second.id)!);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("is empty for a conversation whose rows carry no tag", async () => {
|
|
103
|
+
const untaggedId = "conv-sight-frame-untagged";
|
|
104
|
+
ensureConversation(untaggedId);
|
|
105
|
+
await addMessage(untaggedId, "user", frameContent("att-9"), {
|
|
106
|
+
metadata: { voiceSessionTurn: true },
|
|
107
|
+
});
|
|
108
|
+
await addMessage(untaggedId, "assistant", "sure");
|
|
109
|
+
|
|
110
|
+
expect(selectSightFrameCaptureTimes(untaggedId).size).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("drops a row that only mentions the key in an unrelated value", async () => {
|
|
114
|
+
const mentionId = "conv-sight-frame-mention";
|
|
115
|
+
ensureConversation(mentionId);
|
|
116
|
+
await addMessage(mentionId, "user", "hello", {
|
|
117
|
+
metadata: { backgroundEventSource: "sightFrameAttachmentIds" },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(selectSightFrameCaptureTimes(mentionId).size).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("applySightFrameRetention", () => {
|
|
125
|
+
test("stubs the aged frames of a tagged conversation", async () => {
|
|
126
|
+
const conversationId = "conv-sight-frame-retention";
|
|
127
|
+
ensureConversation(conversationId);
|
|
128
|
+
for (const attachmentId of ["att-a", "att-b", "att-c"]) {
|
|
129
|
+
await addMessage(conversationId, "user", frameContent(attachmentId), {
|
|
130
|
+
metadata: { sightFrameAttachmentIds: [attachmentId] },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const assembled: Message[] = [
|
|
135
|
+
{ role: "user", content: [referenceImage("att-a")] },
|
|
136
|
+
{ role: "assistant", content: [{ type: "text", text: "ok" }] },
|
|
137
|
+
{ role: "user", content: [referenceImage("att-b")] },
|
|
138
|
+
{ role: "user", content: [referenceImage("att-c")] },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const retained = applySightFrameRetention(assembled, conversationId);
|
|
142
|
+
|
|
143
|
+
expect(retained[0].content[0].type).toBe("text");
|
|
144
|
+
expect(retained[2].content[0]).toEqual(referenceImage("att-b"));
|
|
145
|
+
expect(retained[3].content[0]).toEqual(referenceImage("att-c"));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("reaches live inline frames that never reloaded from the DB", async () => {
|
|
149
|
+
const conversationId = "conv-sight-frame-retention-live";
|
|
150
|
+
ensureConversation(conversationId);
|
|
151
|
+
for (const attachmentId of ["live-a", "live-b", "live-c"]) {
|
|
152
|
+
await addMessage(conversationId, "user", frameContent(attachmentId), {
|
|
153
|
+
metadata: { sightFrameAttachmentIds: [attachmentId] },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// What `persistQueuedMessageBody` pushes into `ctx.messages`: inline bytes
|
|
158
|
+
// carrying the id of the row persisted as a reference.
|
|
159
|
+
const liveImage = (
|
|
160
|
+
attachmentId: string,
|
|
161
|
+
): Extract<ContentBlock, { type: "image" }> => ({
|
|
162
|
+
type: "image",
|
|
163
|
+
source: { type: "base64", media_type: "image/jpeg", data: "AAAAAAAA" },
|
|
164
|
+
_attachmentId: attachmentId,
|
|
165
|
+
});
|
|
166
|
+
const assembled: Message[] = [
|
|
167
|
+
{ role: "user", content: [liveImage("live-a")] },
|
|
168
|
+
{ role: "assistant", content: [{ type: "text", text: "ok" }] },
|
|
169
|
+
{ role: "user", content: [liveImage("live-b")] },
|
|
170
|
+
{ role: "user", content: [liveImage("live-c")] },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const retained = applySightFrameRetention(assembled, conversationId);
|
|
174
|
+
|
|
175
|
+
expect(retained[0].content[0].type).toBe("text");
|
|
176
|
+
expect(retained[2].content[0]).toEqual(liveImage("live-b"));
|
|
177
|
+
expect(retained[3].content[0]).toEqual(liveImage("live-c"));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("returns the same array for a conversation with no tagged rows", async () => {
|
|
181
|
+
const conversationId = "conv-sight-frame-retention-untagged";
|
|
182
|
+
ensureConversation(conversationId);
|
|
183
|
+
await addMessage(conversationId, "user", frameContent("att-z"));
|
|
184
|
+
|
|
185
|
+
const assembled: Message[] = [
|
|
186
|
+
{ role: "user", content: [referenceImage("att-z")] },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
expect(applySightFrameRetention(assembled, conversationId)).toBe(assembled);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("configured retention budget", () => {
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
setConfig("sight", {});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/** One tagged standalone frame per row, the shape a keep stream leaves. */
|
|
199
|
+
async function seedFrames(
|
|
200
|
+
conversationId: string,
|
|
201
|
+
count: number,
|
|
202
|
+
): Promise<Message[]> {
|
|
203
|
+
ensureConversation(conversationId);
|
|
204
|
+
const assembled: Message[] = [];
|
|
205
|
+
for (let index = 0; index < count; index += 1) {
|
|
206
|
+
const attachmentId = `${conversationId}-att-${index}`;
|
|
207
|
+
await addMessage(conversationId, "user", frameContent(attachmentId), {
|
|
208
|
+
metadata: { sightFrameAttachmentIds: [attachmentId] },
|
|
209
|
+
});
|
|
210
|
+
assembled.push({
|
|
211
|
+
role: "user",
|
|
212
|
+
content: [referenceImage(attachmentId)],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return assembled;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function imageCount(messages: Message[]): number {
|
|
219
|
+
return messages.filter((message) => message.content[0].type === "image")
|
|
220
|
+
.length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test("keeps the number of frames the workspace configured", async () => {
|
|
224
|
+
setConfig("sight", { keepLatestFrames: 4 });
|
|
225
|
+
const conversationId = "conv-sight-budget-configured";
|
|
226
|
+
const assembled = await seedFrames(conversationId, 6);
|
|
227
|
+
|
|
228
|
+
const retained = applySightFrameRetention(assembled, conversationId);
|
|
229
|
+
|
|
230
|
+
expect(imageCount(retained)).toBe(4);
|
|
231
|
+
expect(retained.length - imageCount(retained)).toBe(2);
|
|
232
|
+
// The survivors are the newest, which is what recency buys the model.
|
|
233
|
+
expect(retained.slice(2).every((m) => m.content[0].type === "image")).toBe(
|
|
234
|
+
true,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("caps a configured value above the ceiling", async () => {
|
|
239
|
+
setConfig("sight", { keepLatestFrames: 99 });
|
|
240
|
+
const conversationId = "conv-sight-budget-capped";
|
|
241
|
+
const assembled = await seedFrames(conversationId, 8);
|
|
242
|
+
|
|
243
|
+
const retained = applySightFrameRetention(assembled, conversationId);
|
|
244
|
+
|
|
245
|
+
expect(imageCount(retained)).toBe(MAX_SIGHT_KEEP_LATEST_FRAMES);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("falls back to the default when the workspace configures nothing", async () => {
|
|
249
|
+
const conversationId = "conv-sight-budget-default";
|
|
250
|
+
const assembled = await seedFrames(conversationId, 5);
|
|
251
|
+
|
|
252
|
+
const retained = applySightFrameRetention(assembled, conversationId);
|
|
253
|
+
|
|
254
|
+
expect(imageCount(retained)).toBe(KEEP_LATEST_SIGHT_FRAMES);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("forked conversations", () => {
|
|
259
|
+
test("a fork's tag covers both its source and cloned attachment ids", async () => {
|
|
260
|
+
// A fork copies `messages.content` verbatim, so its blocks still name the
|
|
261
|
+
// SOURCE attachment ids, while `message_attachments` is re-linked to
|
|
262
|
+
// freshly CLONED rows. The compactor reads the link side and stamps cloned
|
|
263
|
+
// ids onto the frames it rebuilds, so a tag naming only one vocabulary
|
|
264
|
+
// goes blind on the other.
|
|
265
|
+
const db = getDb();
|
|
266
|
+
db.run("DELETE FROM message_attachments");
|
|
267
|
+
db.run("DELETE FROM attachments");
|
|
268
|
+
|
|
269
|
+
const source = createConversation("camera call");
|
|
270
|
+
const uploaded = await uploadAttachment(
|
|
271
|
+
"frame.png",
|
|
272
|
+
"image/png",
|
|
273
|
+
"iVBORw0K",
|
|
274
|
+
);
|
|
275
|
+
const row = await addMessage(
|
|
276
|
+
source.id,
|
|
277
|
+
"user",
|
|
278
|
+
JSON.stringify([
|
|
279
|
+
{
|
|
280
|
+
type: "image",
|
|
281
|
+
source: {
|
|
282
|
+
type: "workspace_ref",
|
|
283
|
+
media_type: "image/png",
|
|
284
|
+
attachmentId: uploaded.id,
|
|
285
|
+
sizeBytes: 8,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
]),
|
|
289
|
+
{
|
|
290
|
+
metadata: { sightFrameAttachmentIds: [uploaded.id] },
|
|
291
|
+
skipIndexing: true,
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
linkAttachmentToMessage(row.id, uploaded.id, 0);
|
|
295
|
+
|
|
296
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
297
|
+
const forkRow = getMessages(fork.id)[0];
|
|
298
|
+
const clonedId = getAttachmentMetadataForMessage(forkRow.id)[0]?.id;
|
|
299
|
+
|
|
300
|
+
// The fork really did clone the attachment under a new id.
|
|
301
|
+
expect(clonedId).toBeDefined();
|
|
302
|
+
expect(clonedId).not.toBe(uploaded.id);
|
|
303
|
+
|
|
304
|
+
const captureTimes = selectSightFrameCaptureTimes(fork.id);
|
|
305
|
+
// Both vocabularies resolve, so a frame is matchable whether it arrived
|
|
306
|
+
// through the copied content or through the compactor's manifest.
|
|
307
|
+
expect(captureTimes.has(uploaded.id)).toBe(true);
|
|
308
|
+
expect(captureTimes.has(clonedId!)).toBe(true);
|
|
309
|
+
|
|
310
|
+
// The source conversation is untouched by the widening.
|
|
311
|
+
expect([...selectSightFrameCaptureTimes(source.id).keys()]).toEqual([
|
|
312
|
+
uploaded.id,
|
|
313
|
+
]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("retention still matches the frames a fork holds directly", async () => {
|
|
317
|
+
const db = getDb();
|
|
318
|
+
db.run("DELETE FROM message_attachments");
|
|
319
|
+
db.run("DELETE FROM attachments");
|
|
320
|
+
|
|
321
|
+
const source = createConversation("camera call 2");
|
|
322
|
+
const ids: string[] = [];
|
|
323
|
+
for (const name of ["a.png", "b.png", "c.png"]) {
|
|
324
|
+
const uploaded = await uploadAttachment(name, "image/png", "iVBORw0K");
|
|
325
|
+
const row = await addMessage(
|
|
326
|
+
source.id,
|
|
327
|
+
"user",
|
|
328
|
+
JSON.stringify([
|
|
329
|
+
{
|
|
330
|
+
type: "image",
|
|
331
|
+
source: {
|
|
332
|
+
type: "workspace_ref",
|
|
333
|
+
media_type: "image/png",
|
|
334
|
+
attachmentId: uploaded.id,
|
|
335
|
+
sizeBytes: 8,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
]),
|
|
339
|
+
{
|
|
340
|
+
metadata: { sightFrameAttachmentIds: [uploaded.id] },
|
|
341
|
+
skipIndexing: true,
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
linkAttachmentToMessage(row.id, uploaded.id, 0);
|
|
345
|
+
ids.push(uploaded.id);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
349
|
+
// The fork's own rows, exactly as its history assembles them.
|
|
350
|
+
const assembled: Message[] = getMessages(fork.id).map((r) => ({
|
|
351
|
+
role: "user" as const,
|
|
352
|
+
content: r.content,
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
const retained = applySightFrameRetention(assembled, fork.id);
|
|
356
|
+
|
|
357
|
+
// Oldest of the three is stubbed; the newest two survive.
|
|
358
|
+
expect(retained[0].content[0].type).toBe("text");
|
|
359
|
+
expect(retained[1].content[0].type).toBe("image");
|
|
360
|
+
expect(retained[2].content[0].type).toBe("image");
|
|
361
|
+
});
|
|
362
|
+
});
|