@vellumai/assistant 0.11.7 → 0.11.8-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persisting
|
|
2
|
+
* Persisting an image that arrives during a live-voice call, on its own.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* shutter
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* image
|
|
4
|
+
* Two kinds arrive this way and both take the same route: a photo the user
|
|
5
|
+
* snapped with the shutter, and an ambient camera frame the client's gate kept.
|
|
6
|
+
* Each lands in the conversation as its own user message, the moment it
|
|
7
|
+
* arrives, and **runs no turn**. That single choice is what makes the order of
|
|
8
|
+
* image and speech irrelevant: whatever the user says next, before or after,
|
|
9
|
+
* is answered by a model whose history already contains the picture.
|
|
10
|
+
*
|
|
11
|
+
* A kept frame carries the sight tag as well, which is what lets retention age
|
|
12
|
+
* it out of the model's context while the transcript keeps it. A shutter photo
|
|
13
|
+
* carries no tag: the user chose to take it, so it is not the retention pass's
|
|
14
|
+
* to trim.
|
|
9
15
|
*
|
|
10
16
|
* The alternatives were both worse and both were tried. Attaching the photo to
|
|
11
17
|
* the *next* spoken turn strands it whenever the user speaks first: the
|
|
@@ -19,17 +25,65 @@
|
|
|
19
25
|
* photo can be stripped where one riding the spoken turn would have survived.
|
|
20
26
|
* Stranding is the worse failure, and it happens every time rather than only
|
|
21
27
|
* under overflow.
|
|
28
|
+
*
|
|
29
|
+
* Persists are serialized per conversation ({@link enqueueStandaloneImagePersist}),
|
|
30
|
+
* because the processing flag they take is a boolean rather than a counted
|
|
31
|
+
* lock. Two images in flight at once interleave across the awaits that sit
|
|
32
|
+
* between reading the flag and taking it, so both read idle and both take it.
|
|
33
|
+
* The first to finish then clears it while the second is still writing, which
|
|
34
|
+
* leaves a spoken turn free to launch into a half-written row, and the second
|
|
35
|
+
* finisher goes on to clear THAT turn's flag. Ambient frames make this
|
|
36
|
+
* routine rather than exotic: they arrive every few seconds for as long as
|
|
37
|
+
* the camera is up, and a shutter photo can land between any two of them. The
|
|
38
|
+
* chain holds the flag to one standalone job at a time, which is what makes
|
|
39
|
+
* the set/clear pairing correct rather than merely usually correct.
|
|
40
|
+
*
|
|
41
|
+
* The flag is taken by {@link acquireProcessingFlag}, which reads it free and
|
|
42
|
+
* takes it in one synchronous step. That closes the half of the turn race this
|
|
43
|
+
* module owns: no await sits between the read and the take, so a turn that
|
|
44
|
+
* starts nearby either loses the flag to a keep that already holds it or holds
|
|
45
|
+
* it before the keep looks, and a keep can never write over a turn that got
|
|
46
|
+
* there first. Per `assistant/AGENTS.md`, a resource more than one caller
|
|
47
|
+
* writes has to be serialised per resource; the chain does that between keeps
|
|
48
|
+
* and the atomic take does it against turns.
|
|
49
|
+
*
|
|
50
|
+
* The other half is not this module's: turn startup takes the flag without
|
|
51
|
+
* first asking whether anyone holds it (`persistUserMessage` in
|
|
52
|
+
* `conversation-messaging.ts`), so a turn beginning while a keep is mid-write
|
|
53
|
+
* still overwrites the keep's hold, and the keep's release then clears the
|
|
54
|
+
* turn's. Fixing that means changing how every turn in the daemon acquires,
|
|
55
|
+
* which belongs in `conversation.ts` rather than here.
|
|
56
|
+
*
|
|
57
|
+
* A persist that throws after its row landed is reported as the success it
|
|
58
|
+
* is, because the transcript has the image and the client's view has to match
|
|
59
|
+
* what a reload will show. The persist unwinds its own push before rethrowing,
|
|
60
|
+
* so the resident history is left not matching the rows; rather than reach
|
|
61
|
+
* into an array `persistQueuedMessageBody` owns, the recovery marks the
|
|
62
|
+
* conversation's history stale, and the next turn's
|
|
63
|
+
* `ensureActorScopedHistory` reloads from the DB and sees the frame.
|
|
64
|
+
*
|
|
65
|
+
* One thing is knowingly left behind: when the failure was a link write, the
|
|
66
|
+
* persisted content can reference an attachment row with no link. The row
|
|
67
|
+
* survives (nothing reclaims on a committed persist) and collection only ever
|
|
68
|
+
* considers ids a caller hands it, so the reference keeps resolving.
|
|
22
69
|
*/
|
|
23
70
|
|
|
24
71
|
import { v7 as uuidv7 } from "uuid";
|
|
25
72
|
|
|
26
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
type PersistMessageOptions,
|
|
75
|
+
persistQueuedMessageBody,
|
|
76
|
+
} from "../daemon/conversation-messaging.js";
|
|
77
|
+
import { findConversation } from "../daemon/conversation-registry.js";
|
|
27
78
|
import { getOrCreateConversation } from "../daemon/conversation-store.js";
|
|
28
79
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
80
|
+
deleteOrphanAttachments,
|
|
81
|
+
resolveAttachmentsForPersist,
|
|
31
82
|
} from "../persistence/attachments-store.js";
|
|
32
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
getMessageById,
|
|
85
|
+
recordConversationPersistedSeq,
|
|
86
|
+
} from "../persistence/conversation-crud.js";
|
|
33
87
|
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
34
88
|
import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
|
|
35
89
|
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
@@ -38,76 +92,252 @@ import { getLogger } from "../util/logger.js";
|
|
|
38
92
|
const log = getLogger("live-voice-photo");
|
|
39
93
|
|
|
40
94
|
/**
|
|
41
|
-
* How long to wait for an in-flight turn before giving up on
|
|
95
|
+
* How long to wait for an in-flight turn before giving up on an image.
|
|
42
96
|
*
|
|
43
97
|
* Persisting takes the conversation's processing lock, which a running turn
|
|
44
98
|
* holds for as long as it runs, tools included. The wait is generous because
|
|
45
99
|
* the alternative is dropping a photo the user watched themselves take, and
|
|
46
|
-
* the
|
|
100
|
+
* the image is not urgent: nothing is blocked on it except the next thing they
|
|
47
101
|
* say.
|
|
48
102
|
*/
|
|
49
103
|
const PROCESSING_WAIT_MS = 30_000;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The wait actually applied. Mutable only so a test can reach the timeout
|
|
107
|
+
* without holding a conversation busy for the full half minute.
|
|
108
|
+
*/
|
|
109
|
+
let processingWaitMs = PROCESSING_WAIT_MS;
|
|
110
|
+
|
|
111
|
+
/** Shorten the idle wait for a test. Returns the value it replaced. */
|
|
112
|
+
export function _setProcessingWaitMsForTests(ms: number): number {
|
|
113
|
+
const previous = processingWaitMs;
|
|
114
|
+
processingWaitMs = ms;
|
|
115
|
+
return previous;
|
|
116
|
+
}
|
|
50
117
|
const PROCESSING_POLL_MS = 100;
|
|
51
118
|
|
|
52
119
|
const PHOTO_MESSAGE_CONTENT = "here's a photo:";
|
|
53
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Text the kept frame's row carries. Neutral rather than first person: nobody
|
|
123
|
+
* spoke this message, the camera simply had something worth showing.
|
|
124
|
+
*/
|
|
125
|
+
const SIGHT_FRAME_MESSAGE_CONTENT = "(camera frame)";
|
|
126
|
+
|
|
54
127
|
export interface LiveVoicePhotoResult {
|
|
55
128
|
readonly ok: boolean;
|
|
56
129
|
readonly messageId?: string;
|
|
57
130
|
}
|
|
58
131
|
|
|
59
132
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
133
|
+
* One conversation's chain of standalone-image persists.
|
|
134
|
+
*
|
|
135
|
+
* `queuedSightFrame` holds the keep that is chained but has not begun, and is
|
|
136
|
+
* what bounds the chain: keeps arrive every few seconds while each job can
|
|
137
|
+
* wait out a turn for {@link PROCESSING_WAIT_MS}, so without replacement the
|
|
138
|
+
* chain grows for as long as a turn runs. A job clears the slot as it starts,
|
|
139
|
+
* so the slot only ever refers to work that can still be called off.
|
|
140
|
+
*/
|
|
141
|
+
interface StandaloneImageQueue {
|
|
142
|
+
tail: Promise<void>;
|
|
143
|
+
queuedSightFrame: { superseded: boolean; attachmentId: string } | null;
|
|
144
|
+
outstanding: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const standaloneImageQueues = new Map<string, StandaloneImageQueue>();
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run `job` after every standalone-image persist already queued for this
|
|
151
|
+
* conversation has settled.
|
|
152
|
+
*
|
|
153
|
+
* Keeps are latest-wins while they wait: a new one supersedes a queued keep
|
|
154
|
+
* that has not begun, which resolves `ok: false` so its caller reports the one
|
|
155
|
+
* lost frame. The camera is about to shoot another anyway, and what matters is
|
|
156
|
+
* the view at the moment the user speaks. Photos are never superseded: the
|
|
157
|
+
* user watched themselves take each one.
|
|
158
|
+
*
|
|
159
|
+
* The map entry is dropped once the chain drains, so a conversation that ends
|
|
160
|
+
* leaves nothing behind.
|
|
63
161
|
*/
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
162
|
+
async function enqueueStandaloneImagePersist(
|
|
163
|
+
conversationId: string,
|
|
164
|
+
attachmentId: string,
|
|
165
|
+
kind: "photo" | "sight_frame",
|
|
166
|
+
job: () => Promise<LiveVoicePhotoResult>,
|
|
167
|
+
): Promise<LiveVoicePhotoResult> {
|
|
168
|
+
let queue = standaloneImageQueues.get(conversationId);
|
|
169
|
+
if (!queue) {
|
|
170
|
+
queue = {
|
|
171
|
+
tail: Promise.resolve(),
|
|
172
|
+
queuedSightFrame: null,
|
|
173
|
+
outstanding: 0,
|
|
174
|
+
};
|
|
175
|
+
standaloneImageQueues.set(conversationId, queue);
|
|
176
|
+
}
|
|
177
|
+
const chained = queue;
|
|
178
|
+
|
|
179
|
+
const ticket = { superseded: false, attachmentId };
|
|
180
|
+
if (kind === "sight_frame") {
|
|
181
|
+
if (chained.queuedSightFrame) {
|
|
182
|
+
chained.queuedSightFrame.superseded = true;
|
|
183
|
+
}
|
|
184
|
+
chained.queuedSightFrame = ticket;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
chained.outstanding += 1;
|
|
188
|
+
const running = chained.tail.then(() => {
|
|
189
|
+
// Started, so nothing can call it off from here on.
|
|
190
|
+
if (chained.queuedSightFrame === ticket) {
|
|
191
|
+
chained.queuedSightFrame = null;
|
|
192
|
+
}
|
|
193
|
+
if (ticket.superseded) {
|
|
194
|
+
log.debug(
|
|
195
|
+
{ conversationId, attachmentId: ticket.attachmentId },
|
|
196
|
+
"A newer camera frame replaced one still waiting to persist",
|
|
197
|
+
);
|
|
198
|
+
reclaimDroppedFrame(ticket.attachmentId);
|
|
199
|
+
return { ok: false };
|
|
200
|
+
}
|
|
201
|
+
return job();
|
|
67
202
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
203
|
+
// The tail must survive a failed job, or one rejection would strand every
|
|
204
|
+
// image queued behind it.
|
|
205
|
+
chained.tail = running.then(
|
|
206
|
+
() => undefined,
|
|
207
|
+
() => undefined,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
return await running;
|
|
212
|
+
} finally {
|
|
213
|
+
chained.outstanding -= 1;
|
|
214
|
+
if (
|
|
215
|
+
chained.outstanding === 0 &&
|
|
216
|
+
standaloneImageQueues.get(conversationId) === chained
|
|
217
|
+
) {
|
|
218
|
+
standaloneImageQueues.delete(conversationId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Give up the upload behind a keep no message will ever carry: one a newer
|
|
225
|
+
* frame replaced before it started, one whose wait for an idle conversation
|
|
226
|
+
* ran out, and one whose write threw.
|
|
227
|
+
*
|
|
228
|
+
* The client uploaded it and then the daemon dropped it, so nothing else will
|
|
229
|
+
* ever collect it: the client's own abandon-delete fires only when the send
|
|
230
|
+
* fails, and attachment collection is candidate-driven with no sweep.
|
|
231
|
+
*
|
|
232
|
+
* Each caller owes the invariant in the first line. The supersede and timeout
|
|
233
|
+
* paths have it by construction, neither having reached the persist; the
|
|
234
|
+
* thrown path establishes it by checking that no row was inserted
|
|
235
|
+
* ({@link messageMayExist}), because a persist can fail after `addMessage`
|
|
236
|
+
* succeeded. {@link deleteOrphanAttachments} being link-aware is the second
|
|
237
|
+
* backstop rather than the only one: a failure between the insert and the link
|
|
238
|
+
* leaves a row that references the attachment with no link to protect it.
|
|
239
|
+
*
|
|
240
|
+
* The reclaim names the id the caller handed in, which is not always the id
|
|
241
|
+
* the row ended up referencing: conversation scoping clones an attachment
|
|
242
|
+
* already linked elsewhere, and the persisted block then names the clone. The
|
|
243
|
+
* insert check still guards the case that matters, because without a clone the
|
|
244
|
+
* two ids are the same one, and that is the dangling reference this protects.
|
|
245
|
+
*
|
|
246
|
+
* Keeps only. A photo is a deliberate upload the user watched themselves make,
|
|
247
|
+
* and one that failed to persist is theirs to retry, not the daemon's to
|
|
248
|
+
* delete.
|
|
249
|
+
*
|
|
250
|
+
* Best effort. Losing a row's bytes is not worth failing a call over.
|
|
251
|
+
*/
|
|
252
|
+
function reclaimDroppedFrame(attachmentId: string): void {
|
|
253
|
+
try {
|
|
254
|
+
deleteOrphanAttachments([attachmentId]);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
log.warn(
|
|
257
|
+
{ err, attachmentId },
|
|
258
|
+
"Could not reclaim a dropped live-voice camera frame",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Conversations with a standalone-image persist still in flight. */
|
|
264
|
+
export function _standaloneImageQueueSizeForTests(): number {
|
|
265
|
+
return standaloneImageQueues.size;
|
|
76
266
|
}
|
|
77
267
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Take the conversation's processing flag once it is free, or return false on
|
|
270
|
+
* timeout having taken nothing.
|
|
271
|
+
*
|
|
272
|
+
* The read and the take are one synchronous step on purpose. Run-to-completion
|
|
273
|
+
* means nothing can interleave between them, so a turn cannot claim the flag
|
|
274
|
+
* in the instant after this reads it free. Splitting them, with the read here
|
|
275
|
+
* and the take in the caller, leaves an await between the two and a keep every
|
|
276
|
+
* few seconds to land in it.
|
|
277
|
+
*
|
|
278
|
+
* Throws only if `setProcessing` does, which it does when the flag's persist
|
|
279
|
+
* fails. That reverts the in-memory flag before rethrowing, so a throw here
|
|
280
|
+
* means nothing was taken and nothing needs releasing.
|
|
281
|
+
*/
|
|
282
|
+
async function acquireProcessingFlag(conversation: {
|
|
80
283
|
isProcessing: () => boolean;
|
|
284
|
+
setProcessing: (value: boolean) => void;
|
|
81
285
|
}): Promise<boolean> {
|
|
82
|
-
const deadline = Date.now() +
|
|
83
|
-
|
|
286
|
+
const deadline = Date.now() + processingWaitMs;
|
|
287
|
+
for (;;) {
|
|
288
|
+
if (!conversation.isProcessing()) {
|
|
289
|
+
conversation.setProcessing(true);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
84
292
|
if (Date.now() >= deadline) {
|
|
85
293
|
return false;
|
|
86
294
|
}
|
|
87
295
|
await new Promise((resolve) => setTimeout(resolve, PROCESSING_POLL_MS));
|
|
88
296
|
}
|
|
89
|
-
return true;
|
|
90
297
|
}
|
|
91
298
|
|
|
92
299
|
/**
|
|
93
|
-
* Persist
|
|
300
|
+
* Persist one image into the conversation as a user message, running no turn.
|
|
94
301
|
*
|
|
95
302
|
* Takes and releases the processing lock the way the built-in slash commands
|
|
96
303
|
* do: they are the existing precedent for "persist a user message and answer
|
|
97
304
|
* without the agent loop", and the lock is what keeps this write from
|
|
98
|
-
* interleaving with a turn's own persist.
|
|
305
|
+
* interleaving with a turn's own persist. An image that arrives mid-reply
|
|
306
|
+
* therefore lands after that reply's rows rather than between them.
|
|
99
307
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
308
|
+
* Queued behind this conversation's other standalone images, so only one of
|
|
309
|
+
* them ever holds the flag. Never throws. An image that cannot be stored must
|
|
310
|
+
* not take the call down with it, and the caller reports the failure to the
|
|
311
|
+
* user instead.
|
|
102
312
|
*/
|
|
103
|
-
|
|
313
|
+
function persistStandaloneImage(
|
|
314
|
+
conversationId: string,
|
|
315
|
+
attachmentId: string,
|
|
316
|
+
kind: "photo" | "sight_frame",
|
|
317
|
+
persistOptions: Omit<PersistMessageOptions, "attachments" | "requestId">,
|
|
318
|
+
): Promise<LiveVoicePhotoResult> {
|
|
319
|
+
return enqueueStandaloneImagePersist(conversationId, attachmentId, kind, () =>
|
|
320
|
+
writeStandaloneImage(conversationId, attachmentId, kind, persistOptions),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function writeStandaloneImage(
|
|
104
325
|
conversationId: string,
|
|
105
326
|
attachmentId: string,
|
|
327
|
+
kind: "photo" | "sight_frame",
|
|
328
|
+
persistOptions: Omit<PersistMessageOptions, "attachments" | "requestId">,
|
|
106
329
|
): Promise<LiveVoicePhotoResult> {
|
|
330
|
+
const { content } = persistOptions;
|
|
331
|
+
// The id the row is inserted under, so a failure can ask whether the insert
|
|
332
|
+
// landed before deciding the frame is safe to reclaim.
|
|
333
|
+
const requestId = uuidv7();
|
|
107
334
|
try {
|
|
108
|
-
const attachments =
|
|
335
|
+
const attachments = resolveAttachmentsForPersist([attachmentId]);
|
|
109
336
|
if (attachments.length === 0) {
|
|
110
|
-
log.warn(
|
|
337
|
+
log.warn(
|
|
338
|
+
{ attachmentId, kind },
|
|
339
|
+
"Live-voice image attachment did not resolve",
|
|
340
|
+
);
|
|
111
341
|
return { ok: false };
|
|
112
342
|
}
|
|
113
343
|
|
|
@@ -116,53 +346,178 @@ export async function persistLiveVoicePhoto(
|
|
|
116
346
|
// A turn holds the lock for its whole run. Waiting rather than queueing:
|
|
117
347
|
// the conversation's queue drains into a turn, which is the one thing this
|
|
118
348
|
// must not cause.
|
|
119
|
-
if (!(await
|
|
349
|
+
if (!(await acquireProcessingFlag(conversation))) {
|
|
120
350
|
log.warn(
|
|
121
|
-
{ conversationId, attachmentId },
|
|
122
|
-
"Live-voice
|
|
351
|
+
{ conversationId, attachmentId, kind },
|
|
352
|
+
"Live-voice image timed out waiting for the conversation to go idle",
|
|
123
353
|
);
|
|
354
|
+
if (kind === "sight_frame") {
|
|
355
|
+
reclaimDroppedFrame(attachmentId);
|
|
356
|
+
}
|
|
124
357
|
return { ok: false };
|
|
125
358
|
}
|
|
126
359
|
|
|
127
|
-
conversation.setProcessing(true);
|
|
128
360
|
try {
|
|
129
361
|
const persisted = await persistQueuedMessageBody(conversation, {
|
|
130
|
-
|
|
362
|
+
...persistOptions,
|
|
131
363
|
attachments,
|
|
132
|
-
requestId
|
|
133
|
-
metadata: {
|
|
134
|
-
// Marks the row as something the user did on a call rather than
|
|
135
|
-
// typed, the same way a voice turn's own user message is marked.
|
|
136
|
-
voiceSessionTurn: true,
|
|
137
|
-
livePhoto: true,
|
|
138
|
-
},
|
|
364
|
+
requestId,
|
|
139
365
|
});
|
|
140
366
|
|
|
141
|
-
|
|
142
|
-
// render this conversation have to be told directly, or the photo does
|
|
143
|
-
// not appear until something else forces a refetch.
|
|
144
|
-
broadcastMessage({
|
|
145
|
-
type: "user_message_echo",
|
|
146
|
-
text: PHOTO_MESSAGE_CONTENT,
|
|
147
|
-
conversationId,
|
|
148
|
-
messageId: persisted.id,
|
|
149
|
-
});
|
|
150
|
-
recordConversationPersistedSeq(conversationId, getCurrentSeq());
|
|
151
|
-
publishConversationMessagesChanged(conversationId);
|
|
367
|
+
announcePersistedImage(conversationId, content, persisted.id);
|
|
152
368
|
|
|
153
369
|
return { ok: true, messageId: persisted.id };
|
|
154
370
|
} finally {
|
|
155
371
|
conversation.setProcessing(false);
|
|
156
372
|
// Anything queued behind the lock we just held still has to run. Without
|
|
157
|
-
// this a message queued during the
|
|
373
|
+
// this a message queued during the image's write sits until the next
|
|
158
374
|
// turn ends.
|
|
159
|
-
void conversation.kickDrainQueue("loop_complete",
|
|
375
|
+
void conversation.kickDrainQueue("loop_complete", `live_voice_${kind}`);
|
|
160
376
|
}
|
|
161
377
|
} catch (err) {
|
|
162
378
|
log.warn(
|
|
163
|
-
{ err, conversationId, attachmentId },
|
|
164
|
-
"Failed to persist a live-voice
|
|
379
|
+
{ err, conversationId, attachmentId, kind },
|
|
380
|
+
"Failed to persist a live-voice image",
|
|
165
381
|
);
|
|
382
|
+
const inserted = insertedMessageState(conversationId, requestId);
|
|
383
|
+
if (inserted === "exists") {
|
|
384
|
+
// The row is in the transcript, so the result has to say so whatever
|
|
385
|
+
// went wrong afterwards. Reporting failure here loses the frame twice
|
|
386
|
+
// over: the client retracts what it showed and stops treating it as
|
|
387
|
+
// pending, and the row it was told never landed appears on the next
|
|
388
|
+
// reload.
|
|
389
|
+
//
|
|
390
|
+
// The persist unwound its own push before rethrowing, so the resident
|
|
391
|
+
// history no longer matches the rows. Same situation a channel edit or
|
|
392
|
+
// reaction leaves behind, and the same fix: the next turn's
|
|
393
|
+
// `ensureActorScopedHistory` reloads instead of reusing what it holds.
|
|
394
|
+
findConversation(conversationId)?.markHistoryStale();
|
|
395
|
+
try {
|
|
396
|
+
announcePersistedImage(conversationId, content, requestId);
|
|
397
|
+
} catch (announceErr) {
|
|
398
|
+
log.warn(
|
|
399
|
+
{ err: announceErr, conversationId, messageId: requestId },
|
|
400
|
+
"Persisted a live-voice image but could not announce it",
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return { ok: true, messageId: requestId };
|
|
404
|
+
}
|
|
405
|
+
if (kind === "sight_frame" && inserted === "absent") {
|
|
406
|
+
reclaimDroppedFrame(attachmentId);
|
|
407
|
+
}
|
|
166
408
|
return { ok: false };
|
|
167
409
|
}
|
|
168
410
|
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Tell the clients rendering this conversation that a row landed.
|
|
414
|
+
*
|
|
415
|
+
* No turn will announce it, so without this the image does not appear until
|
|
416
|
+
* something else forces a refetch.
|
|
417
|
+
*/
|
|
418
|
+
function announcePersistedImage(
|
|
419
|
+
conversationId: string,
|
|
420
|
+
text: string,
|
|
421
|
+
messageId: string,
|
|
422
|
+
): void {
|
|
423
|
+
broadcastMessage({
|
|
424
|
+
type: "user_message_echo",
|
|
425
|
+
text,
|
|
426
|
+
conversationId,
|
|
427
|
+
messageId,
|
|
428
|
+
});
|
|
429
|
+
recordConversationPersistedSeq(conversationId, getCurrentSeq());
|
|
430
|
+
publishConversationMessagesChanged(conversationId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Whether the persist's own row is in the conversation.
|
|
435
|
+
*
|
|
436
|
+
* A persist can fail well after `addMessage` succeeded: a link write that
|
|
437
|
+
* fails is repaired by rewriting the content, and that rewrite can throw in
|
|
438
|
+
* turn.
|
|
439
|
+
*
|
|
440
|
+
* Three-valued because its two readers need opposite answers under doubt, and
|
|
441
|
+
* a boolean can only serve one of them. Reclaiming an attachment on a guess
|
|
442
|
+
* deletes bytes a message may reference; reporting success on a guess tells
|
|
443
|
+
* the client a frame landed when it may not have, and the client stops showing
|
|
444
|
+
* it as pending and stops retrying on the strength of that word. Neither is
|
|
445
|
+
* recoverable, so `unknown` refuses both: never act on a fact that could not
|
|
446
|
+
* be read.
|
|
447
|
+
*
|
|
448
|
+
* A row found under this id is provably this persist's own. The deduplicating
|
|
449
|
+
* branch of `addMessage` fires only for a `clientMessageId`, which no image
|
|
450
|
+
* here sets, and it returns rather than throws, so it cannot leave a half
|
|
451
|
+
* finished persist behind an id that resolves to someone else's row.
|
|
452
|
+
*/
|
|
453
|
+
function insertedMessageState(
|
|
454
|
+
conversationId: string,
|
|
455
|
+
messageId: string,
|
|
456
|
+
): "exists" | "absent" | "unknown" {
|
|
457
|
+
try {
|
|
458
|
+
return getMessageById(messageId, conversationId) !== null
|
|
459
|
+
? "exists"
|
|
460
|
+
: "absent";
|
|
461
|
+
} catch (err) {
|
|
462
|
+
log.warn(
|
|
463
|
+
{ err, conversationId, messageId },
|
|
464
|
+
"Could not tell whether a live-voice image persisted; keeping its attachment and reporting failure",
|
|
465
|
+
);
|
|
466
|
+
return "unknown";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Persist a photo the user took mid-call.
|
|
472
|
+
*
|
|
473
|
+
* Carries `livePhoto`, which is what the client reads to show the snap on its
|
|
474
|
+
* receipt strip, and no sight tag: a deliberate photo is not retention's to
|
|
475
|
+
* age out.
|
|
476
|
+
*/
|
|
477
|
+
export async function persistLiveVoicePhoto(
|
|
478
|
+
conversationId: string,
|
|
479
|
+
attachmentId: string,
|
|
480
|
+
): Promise<LiveVoicePhotoResult> {
|
|
481
|
+
return persistStandaloneImage(conversationId, attachmentId, "photo", {
|
|
482
|
+
content: PHOTO_MESSAGE_CONTENT,
|
|
483
|
+
metadata: {
|
|
484
|
+
// Marks the row as something the user did on a call rather than
|
|
485
|
+
// typed, the same way a voice turn's own user message is marked.
|
|
486
|
+
voiceSessionTurn: true,
|
|
487
|
+
livePhoto: true,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Persist an ambient camera frame the client's gate kept.
|
|
494
|
+
*
|
|
495
|
+
* The sight tag names the attachment on the row that carries it, because an
|
|
496
|
+
* attachment holds no metadata of its own. Retention reads the tag to decide
|
|
497
|
+
* which images a turn still sends in full and which become timestamped stubs,
|
|
498
|
+
* so an untagged frame would sit in every later request forever. The persist
|
|
499
|
+
* stamps it, from the id materialization ended up storing rather than the one
|
|
500
|
+
* asked for here.
|
|
501
|
+
*
|
|
502
|
+
* `scripted` because the camera's gate sent this, not the user: a keep every
|
|
503
|
+
* few seconds would otherwise read downstream as that many turns the user
|
|
504
|
+
* took, and activation counts turns that claim they were typed.
|
|
505
|
+
*/
|
|
506
|
+
export async function persistLiveVoiceSightFrame(
|
|
507
|
+
conversationId: string,
|
|
508
|
+
attachmentId: string,
|
|
509
|
+
): Promise<LiveVoicePhotoResult> {
|
|
510
|
+
return persistStandaloneImage(conversationId, attachmentId, "sight_frame", {
|
|
511
|
+
content: SIGHT_FRAME_MESSAGE_CONTENT,
|
|
512
|
+
metadata: { voiceSessionTurn: true },
|
|
513
|
+
scripted: true,
|
|
514
|
+
// The camera sampled this, nobody sent it. Indexing it would feed
|
|
515
|
+
// extraction a frame every few seconds of whatever the room happens to
|
|
516
|
+
// contain, and commit those visuals to long-term memory with no consent
|
|
517
|
+
// surface: the design puts keeps in the TRANSCRIPT, which the user can see
|
|
518
|
+
// and delete, and says nothing about memory. The text half is worthless to
|
|
519
|
+
// search anyway, every row reading "(camera frame)".
|
|
520
|
+
skipIndexing: true,
|
|
521
|
+
sightFrameAttachmentIds: [attachmentId],
|
|
522
|
+
});
|
|
523
|
+
}
|