@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
|
@@ -15,23 +15,183 @@ import {
|
|
|
15
15
|
deleteConversation,
|
|
16
16
|
setConversation,
|
|
17
17
|
} from "../../daemon/conversation-registry.js";
|
|
18
|
+
import { applySightFrameRetention } from "../../daemon/conversation-runtime-assembly.js";
|
|
18
19
|
import {
|
|
20
|
+
getAttachmentById,
|
|
19
21
|
getAttachmentsForMessage,
|
|
22
|
+
linkAttachmentToMessage,
|
|
20
23
|
uploadAttachment,
|
|
21
24
|
} from "../../persistence/attachments-store.js";
|
|
22
25
|
import {
|
|
26
|
+
addMessage,
|
|
23
27
|
createConversation,
|
|
24
28
|
getMessages,
|
|
29
|
+
type MessageRow,
|
|
30
|
+
selectSightFrameCaptureTimes,
|
|
25
31
|
} from "../../persistence/conversation-crud.js";
|
|
32
|
+
import { sightFrameAttachmentIdsFromMetadata } from "../../persistence/conversation-types.js";
|
|
26
33
|
import { initializeDb } from "../../persistence/db-init.js";
|
|
34
|
+
import { mediaBlockAttachmentId, type Message } from "../../providers/types.js";
|
|
27
35
|
import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
|
|
28
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
_setProcessingWaitMsForTests,
|
|
38
|
+
_standaloneImageQueueSizeForTests,
|
|
39
|
+
persistLiveVoicePhoto,
|
|
40
|
+
persistLiveVoiceSightFrame,
|
|
41
|
+
} from "../live-voice-photo.js";
|
|
29
42
|
|
|
30
43
|
await initializeDb();
|
|
31
44
|
|
|
32
45
|
const IMAGE_BASE64 =
|
|
33
46
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk";
|
|
34
47
|
|
|
48
|
+
/** A registered conversation the persist paths can reach, plus its teardown. */
|
|
49
|
+
function liveConversation(title: string) {
|
|
50
|
+
const conversation = createConversation(title);
|
|
51
|
+
const { provider } = createMockProvider([textResponse("")]);
|
|
52
|
+
const activeConversation = new Conversation(
|
|
53
|
+
conversation.id,
|
|
54
|
+
provider,
|
|
55
|
+
"system prompt",
|
|
56
|
+
() => {},
|
|
57
|
+
"/tmp",
|
|
58
|
+
{ maxTokens: 4096 },
|
|
59
|
+
);
|
|
60
|
+
activeConversation.setTrustContext({
|
|
61
|
+
trustClass: "guardian",
|
|
62
|
+
sourceChannel: "vellum",
|
|
63
|
+
});
|
|
64
|
+
setConversation(conversation.id, activeConversation);
|
|
65
|
+
return {
|
|
66
|
+
id: conversation.id,
|
|
67
|
+
activeConversation,
|
|
68
|
+
dispose: () => {
|
|
69
|
+
deleteConversation(conversation.id);
|
|
70
|
+
activeConversation.dispose();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function metadataOf(row: MessageRow): Record<string, unknown> {
|
|
76
|
+
return JSON.parse(row.metadata ?? "{}") as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sleep(ms: number): Promise<void> {
|
|
80
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Record every `setProcessing` on a conversation and track how many holders
|
|
85
|
+
* the flag believes it has. A boolean flag cannot count, so a depth above one
|
|
86
|
+
* is two writers thinking they own it, and the first to finish clearing it
|
|
87
|
+
* out from under the other.
|
|
88
|
+
*/
|
|
89
|
+
function watchProcessing(conversation: Conversation): {
|
|
90
|
+
readonly calls: boolean[];
|
|
91
|
+
depth: number;
|
|
92
|
+
maxDepth: number;
|
|
93
|
+
} {
|
|
94
|
+
const seen = { calls: [] as boolean[], depth: 0, maxDepth: 0 };
|
|
95
|
+
const original = conversation.setProcessing.bind(conversation);
|
|
96
|
+
conversation.setProcessing = (value: boolean): void => {
|
|
97
|
+
seen.calls.push(value);
|
|
98
|
+
seen.depth += value ? 1 : -1;
|
|
99
|
+
seen.maxDepth = Math.max(seen.maxDepth, seen.depth);
|
|
100
|
+
original(value);
|
|
101
|
+
};
|
|
102
|
+
return seen;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function uploadFrame(name: string): Promise<string> {
|
|
106
|
+
const attachment = await uploadAttachment(name, "image/png", IMAGE_BASE64);
|
|
107
|
+
return attachment.id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True while the attachment's row is still in the store. */
|
|
111
|
+
function frameStored(attachmentId: string): boolean {
|
|
112
|
+
return getAttachmentById(attachmentId) !== null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Attachment ids the conversation's LIVE history is attributable by, which is
|
|
117
|
+
* the array a turn sends rather than the rows a reload would rebuild.
|
|
118
|
+
*/
|
|
119
|
+
function liveImageIds(conversation: Conversation): Array<string | undefined> {
|
|
120
|
+
const ids: Array<string | undefined> = [];
|
|
121
|
+
for (const message of conversation.messages) {
|
|
122
|
+
for (const block of message.content) {
|
|
123
|
+
if (block.type === "image") {
|
|
124
|
+
ids.push(mediaBlockAttachmentId(block));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return ids;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Let a turn try to start in the instant right after the persist reads the
|
|
133
|
+
* flag free. The turn models a correct acquirer: it claims only what it finds
|
|
134
|
+
* free, so it succeeds exactly when the persist left a gap between its read
|
|
135
|
+
* and its take.
|
|
136
|
+
*/
|
|
137
|
+
function armTurnStartInTheGap(conversation: Conversation): {
|
|
138
|
+
claimed: () => boolean;
|
|
139
|
+
} {
|
|
140
|
+
let armed = true;
|
|
141
|
+
let claimed = false;
|
|
142
|
+
const readFlag = conversation.isProcessing.bind(conversation);
|
|
143
|
+
const writeFlag = conversation.setProcessing.bind(conversation);
|
|
144
|
+
conversation.isProcessing = (): boolean => {
|
|
145
|
+
const busy = readFlag();
|
|
146
|
+
if (armed && !busy) {
|
|
147
|
+
armed = false;
|
|
148
|
+
queueMicrotask(() => {
|
|
149
|
+
if (!readFlag()) {
|
|
150
|
+
writeFlag(true);
|
|
151
|
+
claimed = true;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return busy;
|
|
156
|
+
};
|
|
157
|
+
return { claimed: () => claimed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The attachment id the row's persisted image block actually references. */
|
|
161
|
+
function storedImageId(row: MessageRow): string | undefined {
|
|
162
|
+
for (const block of row.content) {
|
|
163
|
+
if (block.type !== "image") {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const id = mediaBlockAttachmentId(block);
|
|
167
|
+
if (id !== undefined) {
|
|
168
|
+
return id;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* An attachment already linked to a message in another conversation, which is
|
|
176
|
+
* what makes the persist clone it rather than reuse the row.
|
|
177
|
+
*/
|
|
178
|
+
async function attachmentLinkedElsewhere(
|
|
179
|
+
otherConversationId: string,
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
const attachment = await uploadAttachment(
|
|
182
|
+
"frame.png",
|
|
183
|
+
"image/png",
|
|
184
|
+
IMAGE_BASE64,
|
|
185
|
+
);
|
|
186
|
+
const elsewhere = await addMessage(
|
|
187
|
+
otherConversationId,
|
|
188
|
+
"user",
|
|
189
|
+
"look at this",
|
|
190
|
+
);
|
|
191
|
+
linkAttachmentToMessage(elsewhere.id, attachment.id, 0);
|
|
192
|
+
return attachment.id;
|
|
193
|
+
}
|
|
194
|
+
|
|
35
195
|
describe("persistLiveVoicePhoto", () => {
|
|
36
196
|
test("persists and echoes the image with human-readable context", async () => {
|
|
37
197
|
const conversation = createConversation("Live voice photo");
|
|
@@ -97,4 +257,480 @@ describe("persistLiveVoicePhoto", () => {
|
|
|
97
257
|
activeConversation.dispose();
|
|
98
258
|
}
|
|
99
259
|
});
|
|
260
|
+
|
|
261
|
+
test("leaves the photo untagged, so retention never ages it out", async () => {
|
|
262
|
+
// A photo the user chose to take is not an ambient frame; the sight tag
|
|
263
|
+
// would hand it to the pass that stubs frames out of the context.
|
|
264
|
+
const live = liveConversation("Live voice photo untagged");
|
|
265
|
+
try {
|
|
266
|
+
const attachment = await uploadAttachment(
|
|
267
|
+
"photo.png",
|
|
268
|
+
"image/png",
|
|
269
|
+
IMAGE_BASE64,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const result = await persistLiveVoicePhoto(live.id, attachment.id);
|
|
273
|
+
expect(result.ok).toBe(true);
|
|
274
|
+
|
|
275
|
+
expect(selectSightFrameCaptureTimes(live.id).size).toBe(0);
|
|
276
|
+
} finally {
|
|
277
|
+
live.dispose();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("leaves the photo unscripted, the shutter being a turn the user took", async () => {
|
|
282
|
+
// Pressing the shutter is the user acting, so the row keeps asserting
|
|
283
|
+
// "the user did this" and stays inside activation.
|
|
284
|
+
const live = liveConversation("Live voice photo scripted");
|
|
285
|
+
try {
|
|
286
|
+
const attachment = await uploadAttachment(
|
|
287
|
+
"photo.png",
|
|
288
|
+
"image/png",
|
|
289
|
+
IMAGE_BASE64,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect((await persistLiveVoicePhoto(live.id, attachment.id)).ok).toBe(
|
|
293
|
+
true,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(metadataOf(getMessages(live.id)[0]).scripted).toBe(false);
|
|
297
|
+
} finally {
|
|
298
|
+
live.dispose();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("persistLiveVoiceSightFrame", () => {
|
|
304
|
+
test("tags the row with the attachment it carries", async () => {
|
|
305
|
+
const live = liveConversation("Live voice sight frame");
|
|
306
|
+
try {
|
|
307
|
+
const attachment = await uploadAttachment(
|
|
308
|
+
"frame.png",
|
|
309
|
+
"image/png",
|
|
310
|
+
IMAGE_BASE64,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const result = await persistLiveVoiceSightFrame(live.id, attachment.id);
|
|
314
|
+
expect(result.ok).toBe(true);
|
|
315
|
+
|
|
316
|
+
const [message] = getMessages(live.id);
|
|
317
|
+
expect(message.content.filter((block) => block.type === "text")).toEqual([
|
|
318
|
+
{ type: "text", text: "(camera frame)" },
|
|
319
|
+
]);
|
|
320
|
+
expect(getAttachmentsForMessage(message.id)).toHaveLength(1);
|
|
321
|
+
// Readable by the retention pass, which is the whole point of the tag.
|
|
322
|
+
expect([...selectSightFrameCaptureTimes(live.id).keys()]).toEqual([
|
|
323
|
+
attachment.id,
|
|
324
|
+
]);
|
|
325
|
+
} finally {
|
|
326
|
+
live.dispose();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("marks the row scripted, so keeps are not counted as turns the user took", async () => {
|
|
331
|
+
// The gate sent this, not the user. A keep every few seconds would
|
|
332
|
+
// otherwise read downstream as that many turns taken, and activation
|
|
333
|
+
// believes a row that claims it was typed.
|
|
334
|
+
const live = liveConversation("Live voice sight frame scripted");
|
|
335
|
+
try {
|
|
336
|
+
const attachment = await uploadAttachment(
|
|
337
|
+
"frame.png",
|
|
338
|
+
"image/png",
|
|
339
|
+
IMAGE_BASE64,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(
|
|
343
|
+
(await persistLiveVoiceSightFrame(live.id, attachment.id)).ok,
|
|
344
|
+
).toBe(true);
|
|
345
|
+
|
|
346
|
+
expect(metadataOf(getMessages(live.id)[0]).scripted).toBe(true);
|
|
347
|
+
} finally {
|
|
348
|
+
live.dispose();
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("tags the id the attachment was cloned into, not the one it arrived as", async () => {
|
|
353
|
+
// An attachment already linked to another conversation is cloned into this
|
|
354
|
+
// one under a fresh id, and both the persisted block and the link carry the
|
|
355
|
+
// clone. A tag naming the id the caller held would match nothing.
|
|
356
|
+
const source = liveConversation("Live voice sight frame source");
|
|
357
|
+
const live = liveConversation("Live voice sight frame clone");
|
|
358
|
+
try {
|
|
359
|
+
const arrivedAs = await attachmentLinkedElsewhere(source.id);
|
|
360
|
+
|
|
361
|
+
expect((await persistLiveVoiceSightFrame(live.id, arrivedAs)).ok).toBe(
|
|
362
|
+
true,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const [row] = getMessages(live.id);
|
|
366
|
+
const stored = storedImageId(row);
|
|
367
|
+
expect(stored).toBeDefined();
|
|
368
|
+
expect(stored).not.toBe(arrivedAs);
|
|
369
|
+
expect(sightFrameAttachmentIdsFromMetadata(metadataOf(row))).toEqual([
|
|
370
|
+
stored!,
|
|
371
|
+
]);
|
|
372
|
+
expect([...selectSightFrameCaptureTimes(live.id).keys()]).toEqual([
|
|
373
|
+
stored!,
|
|
374
|
+
]);
|
|
375
|
+
} finally {
|
|
376
|
+
live.dispose();
|
|
377
|
+
source.dispose();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("the live in-memory block names the cloned id, not the arriving one", async () => {
|
|
382
|
+
// The live message is built from the attachments the caller handed in, so
|
|
383
|
+
// its block names the id the session held while the tag names what
|
|
384
|
+
// materialization stored. Left disagreeing, the turn that created the
|
|
385
|
+
// frame sends it full size and only a reload ever bounds it.
|
|
386
|
+
const source = liveConversation("Live voice live block source");
|
|
387
|
+
const live = liveConversation("Live voice live block clone");
|
|
388
|
+
try {
|
|
389
|
+
const arrivedAs = await attachmentLinkedElsewhere(source.id);
|
|
390
|
+
|
|
391
|
+
expect((await persistLiveVoiceSightFrame(live.id, arrivedAs)).ok).toBe(
|
|
392
|
+
true,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const stored = storedImageId(getMessages(live.id)[0]);
|
|
396
|
+
expect(stored).toBeDefined();
|
|
397
|
+
expect(stored).not.toBe(arrivedAs);
|
|
398
|
+
// The array a turn actually sends agrees with the tag.
|
|
399
|
+
expect(liveImageIds(live.activeConversation)).toEqual([stored]);
|
|
400
|
+
} finally {
|
|
401
|
+
live.dispose();
|
|
402
|
+
source.dispose();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("retention ages a cloned frame in the live history", async () => {
|
|
407
|
+
// The reload path is covered below; this is the same conversation before
|
|
408
|
+
// any reload, which is where a long call spends all its time.
|
|
409
|
+
const source = liveConversation("Live voice live retention source");
|
|
410
|
+
const live = liveConversation("Live voice live retention");
|
|
411
|
+
try {
|
|
412
|
+
const arrivedAs = await attachmentLinkedElsewhere(source.id);
|
|
413
|
+
expect((await persistLiveVoiceSightFrame(live.id, arrivedAs)).ok).toBe(
|
|
414
|
+
true,
|
|
415
|
+
);
|
|
416
|
+
for (const name of ["live-fresh-a.png", "live-fresh-b.png"]) {
|
|
417
|
+
const fresh = await uploadFrame(name);
|
|
418
|
+
expect((await persistLiveVoiceSightFrame(live.id, fresh)).ok).toBe(
|
|
419
|
+
true,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const live_ = live.activeConversation;
|
|
424
|
+
expect(live_.messages).toHaveLength(3);
|
|
425
|
+
const retained = live_.trimAgedSightFrames(live_.messages);
|
|
426
|
+
|
|
427
|
+
// Three frames, a budget of two: the cloned one is the oldest and goes.
|
|
428
|
+
expect(retained[0].content.some((b) => b.type === "image")).toBe(false);
|
|
429
|
+
expect(retained[1].content.some((b) => b.type === "image")).toBe(true);
|
|
430
|
+
expect(retained[2].content.some((b) => b.type === "image")).toBe(true);
|
|
431
|
+
} finally {
|
|
432
|
+
live.dispose();
|
|
433
|
+
source.dispose();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("retention ages a cloned frame, because the tag matches its block", async () => {
|
|
438
|
+
// The end of the same thread: a tag naming the id the caller held leaves
|
|
439
|
+
// this frame unrecognized, so it is never counted and never stubbed, and
|
|
440
|
+
// it rides every later request for the rest of the call.
|
|
441
|
+
const source = liveConversation("Live voice cloned retention source");
|
|
442
|
+
const live = liveConversation("Live voice cloned retention");
|
|
443
|
+
try {
|
|
444
|
+
const arrivedAs = await attachmentLinkedElsewhere(source.id);
|
|
445
|
+
expect((await persistLiveVoiceSightFrame(live.id, arrivedAs)).ok).toBe(
|
|
446
|
+
true,
|
|
447
|
+
);
|
|
448
|
+
for (const name of ["later-a.png", "later-b.png"]) {
|
|
449
|
+
const fresh = await uploadAttachment(name, "image/png", IMAGE_BASE64);
|
|
450
|
+
expect((await persistLiveVoiceSightFrame(live.id, fresh.id)).ok).toBe(
|
|
451
|
+
true,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const assembled: Message[] = getMessages(live.id).map((row) => ({
|
|
456
|
+
role: "user" as const,
|
|
457
|
+
content: row.content,
|
|
458
|
+
}));
|
|
459
|
+
const retained = applySightFrameRetention(assembled, live.id);
|
|
460
|
+
|
|
461
|
+
// Three frames, a budget of two: the cloned one is the oldest and goes.
|
|
462
|
+
expect(retained[0].content.some((b) => b.type === "image")).toBe(false);
|
|
463
|
+
expect(retained[1].content.some((b) => b.type === "image")).toBe(true);
|
|
464
|
+
expect(retained[2].content.some((b) => b.type === "image")).toBe(true);
|
|
465
|
+
} finally {
|
|
466
|
+
live.dispose();
|
|
467
|
+
source.dispose();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("reports a frame whose attachment does not resolve", async () => {
|
|
472
|
+
const live = liveConversation("Live voice sight frame missing");
|
|
473
|
+
try {
|
|
474
|
+
expect(await persistLiveVoiceSightFrame(live.id, "att-missing")).toEqual({
|
|
475
|
+
ok: false,
|
|
476
|
+
});
|
|
477
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
478
|
+
} finally {
|
|
479
|
+
live.dispose();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("waits out an in-flight turn instead of splitting its rows", async () => {
|
|
484
|
+
// A keep landing mid-reply must neither interrupt the reply nor land
|
|
485
|
+
// between the rows the reply persists. The processing lock is what orders
|
|
486
|
+
// them: the keep takes it only once the turn has let go.
|
|
487
|
+
const live = liveConversation("Live voice sight frame interleave");
|
|
488
|
+
try {
|
|
489
|
+
const attachment = await uploadAttachment(
|
|
490
|
+
"frame.png",
|
|
491
|
+
"image/png",
|
|
492
|
+
IMAGE_BASE64,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
live.activeConversation.setProcessing(true);
|
|
496
|
+
const pending = persistLiveVoiceSightFrame(live.id, attachment.id);
|
|
497
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
498
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
499
|
+
|
|
500
|
+
// The turn's own persist, while it still holds the lock.
|
|
501
|
+
await addMessage(live.id, "assistant", "still looking");
|
|
502
|
+
live.activeConversation.setProcessing(false);
|
|
503
|
+
expect(await pending).toMatchObject({ ok: true });
|
|
504
|
+
|
|
505
|
+
const rows = getMessages(live.id);
|
|
506
|
+
expect(rows.map((row) => row.role)).toEqual(["assistant", "user"]);
|
|
507
|
+
} finally {
|
|
508
|
+
live.dispose();
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe("standalone image persists are serialized per conversation", () => {
|
|
514
|
+
test("two images arriving together take the flag one at a time", async () => {
|
|
515
|
+
// The window the boolean flag cannot survive. Both persists interleave
|
|
516
|
+
// across the awaits between reading the flag and taking it, so both read
|
|
517
|
+
// idle and both take it; unserialized the calls come out
|
|
518
|
+
// [true, true, false, false], and that first `false` lands while the
|
|
519
|
+
// second write is still going, leaving a spoken turn free to launch into
|
|
520
|
+
// a half-written row and the second finisher free to clear that turn's
|
|
521
|
+
// flag.
|
|
522
|
+
const live = liveConversation("Live voice concurrent images");
|
|
523
|
+
const seen = watchProcessing(live.activeConversation);
|
|
524
|
+
try {
|
|
525
|
+
const photo = await uploadFrame("snap.png");
|
|
526
|
+
const frame = await uploadFrame("ambient.png");
|
|
527
|
+
|
|
528
|
+
const photoWrite = persistLiveVoicePhoto(live.id, photo);
|
|
529
|
+
const frameWrite = persistLiveVoiceSightFrame(live.id, frame);
|
|
530
|
+
|
|
531
|
+
expect(await photoWrite).toMatchObject({ ok: true });
|
|
532
|
+
expect(await frameWrite).toMatchObject({ ok: true });
|
|
533
|
+
|
|
534
|
+
// One holder at a time, so no release lands while another write is in
|
|
535
|
+
// flight, and every take is answered before the next one.
|
|
536
|
+
expect(seen.calls).toEqual([true, false, true, false]);
|
|
537
|
+
expect(seen.maxDepth).toBe(1);
|
|
538
|
+
expect(seen.depth).toBe(0);
|
|
539
|
+
expect(getMessages(live.id)).toHaveLength(2);
|
|
540
|
+
} finally {
|
|
541
|
+
live.dispose();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("two frames waiting out one turn write one at a time", async () => {
|
|
546
|
+
// The same invariant across the wait path: both jobs sit behind a running
|
|
547
|
+
// turn, and the chain still hands the flag over one at a time when it
|
|
548
|
+
// ends.
|
|
549
|
+
const live = liveConversation("Live voice serialized keeps");
|
|
550
|
+
const seen = watchProcessing(live.activeConversation);
|
|
551
|
+
try {
|
|
552
|
+
const first = await uploadFrame("first.png");
|
|
553
|
+
const second = await uploadFrame("second.png");
|
|
554
|
+
|
|
555
|
+
live.activeConversation.setProcessing(true);
|
|
556
|
+
const firstKeep = persistLiveVoiceSightFrame(live.id, first);
|
|
557
|
+
// Long enough for the first keep to reach the idle wait, so the second
|
|
558
|
+
// queues behind a job that has already begun rather than replacing it.
|
|
559
|
+
await sleep(30);
|
|
560
|
+
const secondKeep = persistLiveVoiceSightFrame(live.id, second);
|
|
561
|
+
await sleep(30);
|
|
562
|
+
live.activeConversation.setProcessing(false);
|
|
563
|
+
|
|
564
|
+
expect(await firstKeep).toMatchObject({ ok: true });
|
|
565
|
+
expect(await secondKeep).toMatchObject({ ok: true });
|
|
566
|
+
|
|
567
|
+
expect(seen.maxDepth).toBe(1);
|
|
568
|
+
expect(seen.depth).toBe(0);
|
|
569
|
+
expect(getMessages(live.id)).toHaveLength(2);
|
|
570
|
+
} finally {
|
|
571
|
+
live.dispose();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("a turn starting in the gap keeps the flag it claimed", async () => {
|
|
576
|
+
// The flag is a boolean with no owner, so reading it free and taking it
|
|
577
|
+
// have to be one step. Split by an await, a turn claims it in between and
|
|
578
|
+
// the frame writes over a turn that got there first, then clears the flag
|
|
579
|
+
// that turn is still holding.
|
|
580
|
+
const live = liveConversation("Live voice turn start race");
|
|
581
|
+
try {
|
|
582
|
+
const frame = await uploadFrame("race.png");
|
|
583
|
+
|
|
584
|
+
// A turn is running, so the keep goes into the wait.
|
|
585
|
+
live.activeConversation.setProcessing(true);
|
|
586
|
+
const keep = persistLiveVoiceSightFrame(live.id, frame);
|
|
587
|
+
await sleep(150);
|
|
588
|
+
|
|
589
|
+
// From here, the next observation of a free flag lets a turn try to
|
|
590
|
+
// start in the following microtask.
|
|
591
|
+
const turn = armTurnStartInTheGap(live.activeConversation);
|
|
592
|
+
live.activeConversation.setProcessing(false);
|
|
593
|
+
|
|
594
|
+
expect(await keep).toMatchObject({ ok: true });
|
|
595
|
+
|
|
596
|
+
// The keep took the flag in the same step it read it free, so the turn
|
|
597
|
+
// found it busy and never claimed it.
|
|
598
|
+
expect(turn.claimed()).toBe(false);
|
|
599
|
+
expect(live.activeConversation.isProcessing()).toBe(false);
|
|
600
|
+
expect(getMessages(live.id)).toHaveLength(1);
|
|
601
|
+
} finally {
|
|
602
|
+
live.dispose();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("a newer keep replaces one that is still waiting to start", async () => {
|
|
607
|
+
// The chain is bounded this way: keeps arrive every few seconds while each
|
|
608
|
+
// job can wait out a turn for far longer, so a queued keep that a newer
|
|
609
|
+
// one has already made stale is given up rather than stacked.
|
|
610
|
+
const live = liveConversation("Live voice coalesced keeps");
|
|
611
|
+
try {
|
|
612
|
+
const running = await uploadFrame("running.png");
|
|
613
|
+
const stale = await uploadFrame("stale.png");
|
|
614
|
+
const newest = await uploadFrame("newest.png");
|
|
615
|
+
|
|
616
|
+
live.activeConversation.setProcessing(true);
|
|
617
|
+
const runningKeep = persistLiveVoiceSightFrame(live.id, running);
|
|
618
|
+
await sleep(30);
|
|
619
|
+
const staleKeep = persistLiveVoiceSightFrame(live.id, stale);
|
|
620
|
+
const newestKeep = persistLiveVoiceSightFrame(live.id, newest);
|
|
621
|
+
await sleep(30);
|
|
622
|
+
live.activeConversation.setProcessing(false);
|
|
623
|
+
|
|
624
|
+
// The one that had already begun still lands; the one it displaced is
|
|
625
|
+
// reported to its caller as the single lost frame it is.
|
|
626
|
+
expect(await runningKeep).toMatchObject({ ok: true });
|
|
627
|
+
expect(await staleKeep).toEqual({ ok: false });
|
|
628
|
+
expect(await newestKeep).toMatchObject({ ok: true });
|
|
629
|
+
|
|
630
|
+
expect(
|
|
631
|
+
[...selectSightFrameCaptureTimes(live.id).keys()].sort(),
|
|
632
|
+
).toHaveLength(2);
|
|
633
|
+
expect(getMessages(live.id)).toHaveLength(2);
|
|
634
|
+
|
|
635
|
+
// The client uploaded the displaced frame and the daemon chose to drop
|
|
636
|
+
// it, so this is the only thing that will ever collect it: the client's
|
|
637
|
+
// own abandon-delete fires on a refused send, and this send succeeded.
|
|
638
|
+
expect(frameStored(stale)).toBe(false);
|
|
639
|
+
// The two that reached a message are not this reclaim's to touch.
|
|
640
|
+
expect(frameStored(running)).toBe(true);
|
|
641
|
+
expect(frameStored(newest)).toBe(true);
|
|
642
|
+
} finally {
|
|
643
|
+
live.dispose();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("queued photos are never replaced", async () => {
|
|
648
|
+
// The user watched themselves take each one, so none of them is stale.
|
|
649
|
+
const live = liveConversation("Live voice queued photos");
|
|
650
|
+
try {
|
|
651
|
+
const ids = [
|
|
652
|
+
await uploadFrame("one.png"),
|
|
653
|
+
await uploadFrame("two.png"),
|
|
654
|
+
await uploadFrame("three.png"),
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
live.activeConversation.setProcessing(true);
|
|
658
|
+
const writes = ids.map((id) => persistLiveVoicePhoto(live.id, id));
|
|
659
|
+
await sleep(30);
|
|
660
|
+
live.activeConversation.setProcessing(false);
|
|
661
|
+
|
|
662
|
+
for (const result of await Promise.all(writes)) {
|
|
663
|
+
expect(result).toMatchObject({ ok: true });
|
|
664
|
+
}
|
|
665
|
+
expect(getMessages(live.id)).toHaveLength(3);
|
|
666
|
+
// Nothing is ever reclaimed on the photo path: none of them was dropped.
|
|
667
|
+
for (const id of ids) {
|
|
668
|
+
expect(frameStored(id)).toBe(true);
|
|
669
|
+
}
|
|
670
|
+
} finally {
|
|
671
|
+
live.dispose();
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("a keep whose wait runs out gives up its upload", async () => {
|
|
676
|
+
// A turn longer than the wait drops the keep, and nothing else would ever
|
|
677
|
+
// collect the row: the client's abandon-delete fires on a refused send,
|
|
678
|
+
// and this send was accepted.
|
|
679
|
+
const restoreWait = _setProcessingWaitMsForTests(120);
|
|
680
|
+
const live = liveConversation("Live voice keep wait timeout");
|
|
681
|
+
try {
|
|
682
|
+
const frame = await uploadFrame("timed-out.png");
|
|
683
|
+
|
|
684
|
+
// A turn that outlasts the wait.
|
|
685
|
+
live.activeConversation.setProcessing(true);
|
|
686
|
+
expect(await persistLiveVoiceSightFrame(live.id, frame)).toEqual({
|
|
687
|
+
ok: false,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
expect(frameStored(frame)).toBe(false);
|
|
691
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
692
|
+
} finally {
|
|
693
|
+
live.activeConversation.setProcessing(false);
|
|
694
|
+
live.dispose();
|
|
695
|
+
_setProcessingWaitMsForTests(restoreWait);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("a photo whose wait runs out keeps its upload", async () => {
|
|
700
|
+
// A photo is a deliberate upload the user watched themselves make, so a
|
|
701
|
+
// failed persist is theirs to retry rather than the daemon's to delete.
|
|
702
|
+
const restoreWait = _setProcessingWaitMsForTests(120);
|
|
703
|
+
const live = liveConversation("Live voice photo wait timeout");
|
|
704
|
+
try {
|
|
705
|
+
const photo = await uploadFrame("timed-out-photo.png");
|
|
706
|
+
|
|
707
|
+
live.activeConversation.setProcessing(true);
|
|
708
|
+
expect(await persistLiveVoicePhoto(live.id, photo)).toEqual({
|
|
709
|
+
ok: false,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
expect(frameStored(photo)).toBe(true);
|
|
713
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
714
|
+
} finally {
|
|
715
|
+
live.activeConversation.setProcessing(false);
|
|
716
|
+
live.dispose();
|
|
717
|
+
_setProcessingWaitMsForTests(restoreWait);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("the conversation's chain is dropped once it drains", async () => {
|
|
722
|
+
const live = liveConversation("Live voice chain cleanup");
|
|
723
|
+
try {
|
|
724
|
+
const frame = await uploadFrame("cleanup.png");
|
|
725
|
+
|
|
726
|
+
const pending = persistLiveVoiceSightFrame(live.id, frame);
|
|
727
|
+
expect(_standaloneImageQueueSizeForTests()).toBe(1);
|
|
728
|
+
expect(await pending).toMatchObject({ ok: true });
|
|
729
|
+
|
|
730
|
+
// Nothing is left behind for a call that ended.
|
|
731
|
+
expect(_standaloneImageQueueSizeForTests()).toBe(0);
|
|
732
|
+
} finally {
|
|
733
|
+
live.dispose();
|
|
734
|
+
}
|
|
735
|
+
});
|
|
100
736
|
});
|