@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
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
attachmentExists,
|
|
38
38
|
AttachmentUploadError,
|
|
39
39
|
createInlineAttachment,
|
|
40
|
+
deleteOrphanAttachments,
|
|
40
41
|
getAttachmentContent,
|
|
41
42
|
getFilePathForAttachment,
|
|
42
43
|
linkAttachmentToMessage,
|
|
@@ -59,7 +60,12 @@ import {
|
|
|
59
60
|
syncMessageToDisk,
|
|
60
61
|
updateMetaFile,
|
|
61
62
|
} from "../persistence/conversation-disk-view.js";
|
|
62
|
-
import
|
|
63
|
+
import { SIGHT_FRAME_ATTACHMENT_IDS_KEY } from "../persistence/conversation-types.js";
|
|
64
|
+
import {
|
|
65
|
+
attachmentIdFragment,
|
|
66
|
+
type ContentBlock,
|
|
67
|
+
type Message,
|
|
68
|
+
} from "../providers/types.js";
|
|
63
69
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
64
70
|
import { getLogger } from "../util/logger.js";
|
|
65
71
|
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
@@ -430,37 +436,232 @@ async function prepareUserAttachmentReferences(
|
|
|
430
436
|
attachments: MessageAttachmentInput[],
|
|
431
437
|
): Promise<PreparedUserAttachment[]> {
|
|
432
438
|
const prepared: PreparedUserAttachment[] = [];
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
+
// Rows this call brought into being, tracked as they are made rather than
|
|
440
|
+
// read back off `prepared`: building a reference block is awaited between
|
|
441
|
+
// creating the row and recording it, so a throw there leaves a row nothing
|
|
442
|
+
// else knows about. The caller only ever sees a returned array, so a throw
|
|
443
|
+
// out of here makes this the one place that can still give them up.
|
|
444
|
+
const created: string[] = [];
|
|
445
|
+
try {
|
|
446
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
447
|
+
const a = attachments[i];
|
|
448
|
+
const outcome = await materializeUserAttachment(
|
|
449
|
+
conversationId,
|
|
450
|
+
conversationCreatedAt,
|
|
451
|
+
a,
|
|
452
|
+
);
|
|
453
|
+
if (outcome.kind === "stored") {
|
|
454
|
+
if (outcome.stored.id !== a.id) {
|
|
455
|
+
created.push(outcome.stored.id);
|
|
456
|
+
}
|
|
457
|
+
prepared.push({
|
|
458
|
+
position: i,
|
|
459
|
+
block: await referenceBlockForAttachment(a, outcome.stored),
|
|
460
|
+
link: { attachmentId: outcome.stored.id },
|
|
461
|
+
});
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (outcome.kind === "rejected") {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
// transient: keep the upload by inlining its bytes (dropped only when the
|
|
468
|
+
// recoverable failure left us with no bytes to inline).
|
|
469
|
+
const inline = await inlineBlockForAttachment(a);
|
|
470
|
+
if (inline) {
|
|
471
|
+
prepared.push({ position: i, block: inline });
|
|
472
|
+
} else {
|
|
473
|
+
log.warn(
|
|
474
|
+
{ filename: a.filename },
|
|
475
|
+
"Dropping user attachment: store write failed and no inline bytes",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
discardAttemptAttachments(created);
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
return prepared;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Give up attachment rows a failed persist attempt created for itself.
|
|
488
|
+
*
|
|
489
|
+
* Conversation scoping CLONES an attachment already linked to another
|
|
490
|
+
* conversation, and an inline upload creates a fresh row under an id of its
|
|
491
|
+
* own. Either way the row is an artifact of this attempt that nothing else can
|
|
492
|
+
* reach: no message names it, and the caller still holds the id it sent or the
|
|
493
|
+
* bytes behind it. An attempt that dies before its message row exists has to
|
|
494
|
+
* take those with it, or the row and its copied file stay for good.
|
|
495
|
+
*
|
|
496
|
+
* Never the id the caller handed in. When materialization stored the
|
|
497
|
+
* attachment under that same id, the row IS the caller's upload, and deleting
|
|
498
|
+
* it would break the retry they are about to make.
|
|
499
|
+
*
|
|
500
|
+
* Routed through {@link deleteOrphanAttachments} so link-awareness stays the
|
|
501
|
+
* backstop, and swallowed on failure: cleaning up must never mask the error
|
|
502
|
+
* that caused it.
|
|
503
|
+
*/
|
|
504
|
+
function discardAttemptAttachments(attachmentIds: string[]): void {
|
|
505
|
+
if (attachmentIds.length === 0) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
deleteOrphanAttachments(attachmentIds);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
log.warn(
|
|
512
|
+
{ err, attachmentIds },
|
|
513
|
+
"Could not discard the attachments of a failed persist attempt",
|
|
439
514
|
);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Materialized ids that differ from the id their input arrived with, which is
|
|
520
|
+
* what separates a row this attempt made from the caller's own upload.
|
|
521
|
+
*/
|
|
522
|
+
function attemptCreatedAttachmentIds(
|
|
523
|
+
attachmentInputs: MessageAttachmentInput[],
|
|
524
|
+
prepared: PreparedUserAttachment[],
|
|
525
|
+
): string[] {
|
|
526
|
+
const created: string[] = [];
|
|
527
|
+
for (const p of prepared) {
|
|
528
|
+
if (!p.link) {
|
|
446
529
|
continue;
|
|
447
530
|
}
|
|
448
|
-
if (
|
|
449
|
-
|
|
531
|
+
if (p.link.attachmentId !== attachmentInputs[p.position]?.id) {
|
|
532
|
+
created.push(p.link.attachmentId);
|
|
450
533
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
534
|
+
}
|
|
535
|
+
return created;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** One requested camera frame: the id the caller held, and what was stored. */
|
|
539
|
+
interface SightFrameEntry {
|
|
540
|
+
inputId: string;
|
|
541
|
+
storedId: string;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Pair each requested ambient camera frame with the id its persisted block is
|
|
546
|
+
* attributable by, walking every materialized attachment back to the input it
|
|
547
|
+
* came from.
|
|
548
|
+
*
|
|
549
|
+
* Which id that is depends on how the attachment landed. A stored one is named
|
|
550
|
+
* by its linked row, which is also what its `workspace_ref` block carries. One
|
|
551
|
+
* that fell back to inline base64 has no row, and its block carries the
|
|
552
|
+
* caller's own id on `_attachmentId`, so that is the id to name: retention
|
|
553
|
+
* reads both shapes through `mediaBlockAttachmentId`, and an inline frame is
|
|
554
|
+
* the heaviest thing in every later request precisely because its bytes are in
|
|
555
|
+
* the row.
|
|
556
|
+
*/
|
|
557
|
+
function requestedSightFrameEntries(
|
|
558
|
+
requestedIds: readonly string[] | undefined,
|
|
559
|
+
attachmentInputs: MessageAttachmentInput[],
|
|
560
|
+
prepared: PreparedUserAttachment[],
|
|
561
|
+
): SightFrameEntry[] {
|
|
562
|
+
if (!requestedIds || requestedIds.length === 0) {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
const requested = new Set(requestedIds);
|
|
566
|
+
const entries: SightFrameEntry[] = [];
|
|
567
|
+
for (const p of prepared) {
|
|
568
|
+
const inputId = attachmentInputs[p.position]?.id;
|
|
569
|
+
if (inputId === undefined || !requested.has(inputId)) {
|
|
570
|
+
continue;
|
|
461
571
|
}
|
|
572
|
+
entries.push({
|
|
573
|
+
inputId,
|
|
574
|
+
storedId: p.link ? p.link.attachmentId : inputId,
|
|
575
|
+
});
|
|
462
576
|
}
|
|
463
|
-
return
|
|
577
|
+
return entries;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* The ids to tag the row with.
|
|
582
|
+
*
|
|
583
|
+
* Exactly one id per frame, never both. A stored attachment's pre-clone id
|
|
584
|
+
* belongs to a DIFFERENT conversation's row, so naming it as well would mark
|
|
585
|
+
* that attachment as an ambient frame if it ever entered this lineage and stub
|
|
586
|
+
* an image someone deliberately attached. That is what separates this from a
|
|
587
|
+
* fork's widened tag (`widenForkSightFrameTags`), where the two ids are two
|
|
588
|
+
* names for the same image.
|
|
589
|
+
*/
|
|
590
|
+
function sightFrameTagIds(entries: SightFrameEntry[]): string[] {
|
|
591
|
+
return [...new Set(entries.map((entry) => entry.storedId))];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Frames whose stored id differs from the one the caller handed in, by the
|
|
596
|
+
* caller's id. Empty unless conversation scoping cloned a row.
|
|
597
|
+
*/
|
|
598
|
+
function sightFrameBlockRenames(
|
|
599
|
+
entries: SightFrameEntry[],
|
|
600
|
+
): Map<string, string> {
|
|
601
|
+
return new Map(
|
|
602
|
+
entries
|
|
603
|
+
.filter((entry) => entry.storedId !== entry.inputId)
|
|
604
|
+
.map((entry) => [entry.inputId, entry.storedId]),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Point an inline media block at the id its attachment was stored under.
|
|
610
|
+
*
|
|
611
|
+
* Only the inline shape is restamped. A `workspace_ref` names its row inside
|
|
612
|
+
* `source`, which materialization already filled with the stored id, so there
|
|
613
|
+
* is nothing to correct and rewriting `_attachmentId` alone would leave the two
|
|
614
|
+
* halves disagreeing.
|
|
615
|
+
*/
|
|
616
|
+
function restampInlineAttachmentId(
|
|
617
|
+
block: ContentBlock,
|
|
618
|
+
renames: ReadonlyMap<string, string>,
|
|
619
|
+
): ContentBlock {
|
|
620
|
+
if (block.type !== "image" && block.type !== "file") {
|
|
621
|
+
return block;
|
|
622
|
+
}
|
|
623
|
+
if (block.source.type === "workspace_ref") {
|
|
624
|
+
return block;
|
|
625
|
+
}
|
|
626
|
+
const renamed =
|
|
627
|
+
block._attachmentId === undefined
|
|
628
|
+
? undefined
|
|
629
|
+
: renames.get(block._attachmentId);
|
|
630
|
+
if (renamed === undefined) {
|
|
631
|
+
return block;
|
|
632
|
+
}
|
|
633
|
+
return { ...block, ...attachmentIdFragment(renamed) };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Rewrite the live message's camera-frame blocks to the ids their rows were
|
|
638
|
+
* stored under.
|
|
639
|
+
*
|
|
640
|
+
* The in-memory message is built from the attachments the caller handed in, so
|
|
641
|
+
* its blocks name the ids the caller held while the tag names what
|
|
642
|
+
* materialization stored. Those differ whenever conversation scoping cloned a
|
|
643
|
+
* row, and retention walking the live history would then find no frame to age:
|
|
644
|
+
* the tag matches nothing until a reload rebuilds the blocks from the persisted
|
|
645
|
+
* content. Restamping is what makes the array a turn actually sends agree with
|
|
646
|
+
* the tag, so a cloned frame is bounded on the turn that created it rather than
|
|
647
|
+
* only after a restart.
|
|
648
|
+
*
|
|
649
|
+
* Matched by id, never by position: `attachmentsToContentBlocks` emits nothing
|
|
650
|
+
* for an attachment it cannot turn into a block, so positions do not line up.
|
|
651
|
+
*/
|
|
652
|
+
function restampInlineAttachmentIds(
|
|
653
|
+
message: Message,
|
|
654
|
+
renames: ReadonlyMap<string, string>,
|
|
655
|
+
): Message {
|
|
656
|
+
if (renames.size === 0) {
|
|
657
|
+
return message;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
...message,
|
|
661
|
+
content: message.content.map((block) =>
|
|
662
|
+
restampInlineAttachmentId(block, renames),
|
|
663
|
+
),
|
|
664
|
+
};
|
|
464
665
|
}
|
|
465
666
|
|
|
466
667
|
function extractTurnChannelContext(
|
|
@@ -736,6 +937,30 @@ export interface PersistMessageOptions {
|
|
|
736
937
|
* what a consumer that must not misattribute a turn to a surface needs.
|
|
737
938
|
*/
|
|
738
939
|
requestClientOs?: string;
|
|
940
|
+
/**
|
|
941
|
+
* Which of `attachments`, by the id the caller holds, arrived as ambient
|
|
942
|
+
* camera frames rather than files the user picked. Stamps
|
|
943
|
+
* `SIGHT_FRAME_ATTACHMENT_IDS_KEY` on the persisted row, which is what the
|
|
944
|
+
* retention pass reads to decide which images a later turn still sends in
|
|
945
|
+
* full and which become timestamped stubs.
|
|
946
|
+
*
|
|
947
|
+
* Named through this option rather than written into `metadata` by the
|
|
948
|
+
* caller because only the persist knows the id the row ends up linked to.
|
|
949
|
+
* A pre-uploaded attachment already linked to another conversation is
|
|
950
|
+
* CLONED into this one under a fresh id, and both the persisted content
|
|
951
|
+
* block and the `message_attachments` row carry the clone. A tag composed
|
|
952
|
+
* ahead of that names a row the retention pass can never match, and the
|
|
953
|
+
* frame then rides every later request forever.
|
|
954
|
+
*
|
|
955
|
+
* The tag names whatever id the persisted block is attributable by: the
|
|
956
|
+
* linked row's id for a materialized attachment, and the caller's own id
|
|
957
|
+
* for one a recoverable store failure left as inline base64, which is the
|
|
958
|
+
* id `attachmentsToContentBlocks` stamps on that block. The inline case is
|
|
959
|
+
* the one that matters most, because such a block carries its full bytes in
|
|
960
|
+
* `messages.content`, so the frame heaviest in every later request is
|
|
961
|
+
* exactly the one retention has to be able to stub.
|
|
962
|
+
*/
|
|
963
|
+
sightFrameAttachmentIds?: readonly string[];
|
|
739
964
|
}
|
|
740
965
|
|
|
741
966
|
// ── persistUserMessage ───────────────────────────────────────────────
|
|
@@ -857,6 +1082,13 @@ export async function persistQueuedMessageBody(
|
|
|
857
1082
|
}),
|
|
858
1083
|
);
|
|
859
1084
|
let pushedToHistory = false;
|
|
1085
|
+
// Hoisted so the failure path can tell which rows this attempt made, and
|
|
1086
|
+
// whether its message ever landed. From the insert onward the persisted
|
|
1087
|
+
// content references those rows while no link yet protects them, which is
|
|
1088
|
+
// exactly the shape a link-aware delete reads as collectible, so the cleanup
|
|
1089
|
+
// must stop at the insert.
|
|
1090
|
+
let preparedAttachments: PreparedUserAttachment[] = [];
|
|
1091
|
+
let messageInserted = false;
|
|
860
1092
|
|
|
861
1093
|
try {
|
|
862
1094
|
const turnCtx =
|
|
@@ -1009,7 +1241,7 @@ export async function persistQueuedMessageBody(
|
|
|
1009
1241
|
// once the message id exists.
|
|
1010
1242
|
const conversationCreatedAt =
|
|
1011
1243
|
getConversation(ctx.conversationId)?.createdAt ?? Date.now();
|
|
1012
|
-
|
|
1244
|
+
preparedAttachments = await prepareUserAttachmentReferences(
|
|
1013
1245
|
ctx.conversationId,
|
|
1014
1246
|
conversationCreatedAt,
|
|
1015
1247
|
attachmentInputs,
|
|
@@ -1021,7 +1253,18 @@ export async function persistQueuedMessageBody(
|
|
|
1021
1253
|
const sentAttachments = preparedAttachments.map(
|
|
1022
1254
|
(p) => attachmentInputs[p.position],
|
|
1023
1255
|
);
|
|
1024
|
-
const
|
|
1256
|
+
const sightFrameEntries = requestedSightFrameEntries(
|
|
1257
|
+
options.sightFrameAttachmentIds,
|
|
1258
|
+
attachmentInputs,
|
|
1259
|
+
preparedAttachments,
|
|
1260
|
+
);
|
|
1261
|
+
const sightFrameRenames = sightFrameBlockRenames(sightFrameEntries);
|
|
1262
|
+
// The blocks are built from the ids the caller held, so a cloned frame is
|
|
1263
|
+
// pointed at its stored row before the message reaches the live history.
|
|
1264
|
+
const cleanMessage = restampInlineAttachmentIds(
|
|
1265
|
+
await createUserMessage(content, sentAttachments),
|
|
1266
|
+
sightFrameRenames,
|
|
1267
|
+
);
|
|
1025
1268
|
|
|
1026
1269
|
// When displayContent is provided (e.g. original text before recording
|
|
1027
1270
|
// intent stripping), persist that to DB so users see the full message
|
|
@@ -1032,17 +1275,26 @@ export async function persistQueuedMessageBody(
|
|
|
1032
1275
|
displayContent,
|
|
1033
1276
|
preparedAttachments.map((p) => p.block),
|
|
1034
1277
|
);
|
|
1278
|
+
// Composed here rather than with the rest of the metadata above, because
|
|
1279
|
+
// materialization is what decides the id each frame is stored under.
|
|
1280
|
+
const sightFrameIds = sightFrameTagIds(sightFrameEntries);
|
|
1281
|
+
const metadataToPersist =
|
|
1282
|
+
sightFrameIds.length > 0
|
|
1283
|
+
? { ...mergedMetadata, [SIGHT_FRAME_ATTACHMENT_IDS_KEY]: sightFrameIds }
|
|
1284
|
+
: mergedMetadata;
|
|
1285
|
+
|
|
1035
1286
|
const persistedUserMessage = await addMessage(
|
|
1036
1287
|
ctx.conversationId,
|
|
1037
1288
|
"user",
|
|
1038
1289
|
contentToPersist,
|
|
1039
1290
|
{
|
|
1040
|
-
metadata:
|
|
1291
|
+
metadata: metadataToPersist,
|
|
1041
1292
|
clientMessageId,
|
|
1042
1293
|
id: requestId,
|
|
1043
1294
|
...(skipIndexing ? { skipIndexing: true } : {}),
|
|
1044
1295
|
},
|
|
1045
1296
|
);
|
|
1297
|
+
messageInserted = true;
|
|
1046
1298
|
|
|
1047
1299
|
if (persistedUserMessage.deduplicated) {
|
|
1048
1300
|
return { id: persistedUserMessage.id, deduplicated: true };
|
|
@@ -1106,7 +1358,13 @@ export async function persistQueuedMessageBody(
|
|
|
1106
1358
|
);
|
|
1107
1359
|
if (inline) {
|
|
1108
1360
|
repairedBlocks ??= preparedAttachments.map((pp) => pp.block);
|
|
1109
|
-
|
|
1361
|
+
// Rebuilt from the caller's attachment, so it names the id the
|
|
1362
|
+
// caller held while the tag written above names the stored row.
|
|
1363
|
+
// Restamping keeps a repaired frame matchable by retention.
|
|
1364
|
+
repairedBlocks[idx] = restampInlineAttachmentId(
|
|
1365
|
+
inline,
|
|
1366
|
+
sightFrameRenames,
|
|
1367
|
+
);
|
|
1110
1368
|
}
|
|
1111
1369
|
}
|
|
1112
1370
|
}
|
|
@@ -1169,6 +1427,11 @@ export async function persistQueuedMessageBody(
|
|
|
1169
1427
|
if (pushedToHistory) {
|
|
1170
1428
|
ctx.messages.pop();
|
|
1171
1429
|
}
|
|
1430
|
+
if (!messageInserted) {
|
|
1431
|
+
discardAttemptAttachments(
|
|
1432
|
+
attemptCreatedAttachmentIds(attachmentInputs, preparedAttachments),
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1172
1435
|
throw err;
|
|
1173
1436
|
}
|
|
1174
1437
|
}
|
|
@@ -48,6 +48,7 @@ import { isGuardianCardRow } from "../notifications/approval-card-data.js";
|
|
|
48
48
|
import {
|
|
49
49
|
getMessages as defaultGetMessages,
|
|
50
50
|
type MessageRow,
|
|
51
|
+
selectSightFrameCaptureTimes,
|
|
51
52
|
} from "../persistence/conversation-crud.js";
|
|
52
53
|
import { isBackgroundConversationType } from "../persistence/conversation-types.js";
|
|
53
54
|
import { createContextSummaryMessage } from "../plugins/defaults/compaction/window-manager.js";
|
|
@@ -76,8 +77,14 @@ import { resolveCapabilities } from "../runtime/capabilities.js";
|
|
|
76
77
|
import type { SubagentState } from "../subagent/types.js";
|
|
77
78
|
import { TERMINAL_STATUSES } from "../subagent/types.js";
|
|
78
79
|
import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
|
|
80
|
+
import { getLogger } from "../util/logger.js";
|
|
79
81
|
import { channelSupportsInlineOptions } from "./channel-ui-capability.js";
|
|
80
82
|
import { findConversationOrSubagent } from "./conversation-registry.js";
|
|
83
|
+
import {
|
|
84
|
+
hasAttributableImages,
|
|
85
|
+
resolveSightKeepLatestFrames,
|
|
86
|
+
stripAgedSightFrames,
|
|
87
|
+
} from "./conversation-sight-frames.js";
|
|
81
88
|
import type { SurfaceShowPair } from "./conversation-surfaces.js";
|
|
82
89
|
import { canonicalizeTimeZone, formatTurnTimestamp } from "./date-context.js";
|
|
83
90
|
import type {} from "./message-protocol.js";
|
|
@@ -89,6 +96,8 @@ import { timeLatencySubSpan } from "./turn-latency-sub-spans.js";
|
|
|
89
96
|
// loop can own it; re-exported here for this module's existing consumers.
|
|
90
97
|
export { stripInjectionsForCompaction } from "../context/strip-injections.js";
|
|
91
98
|
|
|
99
|
+
const log = getLogger("conversation-runtime-assembly");
|
|
100
|
+
|
|
92
101
|
/**
|
|
93
102
|
* Describes the capabilities of the channel through which the user is
|
|
94
103
|
* interacting. Used to gate UI-specific references and permission asks.
|
|
@@ -737,6 +746,64 @@ export function stripNowScratchpad(messages: Message[]): Message[] {
|
|
|
737
746
|
return stripUserTextBlocksByPrefix(messages, NOW_SCRATCHPAD_STRIP_PREFIXES);
|
|
738
747
|
}
|
|
739
748
|
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
// Camera-frame retention
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Trim the conversation's ambient camera frames down to the newest
|
|
755
|
+
* `sight.keepLatestFrames`, replacing the rest with text stubs.
|
|
756
|
+
*
|
|
757
|
+
* A camera call samples a frame every few seconds, and history resends
|
|
758
|
+
* every inline image on every later request, so an hour-long call otherwise
|
|
759
|
+
* grows its own context until the provider rejects it. Trimming here, on the
|
|
760
|
+
* assembled copy the turn is about to send, keeps the stored transcript and its
|
|
761
|
+
* attachments intact.
|
|
762
|
+
*
|
|
763
|
+
* The tag that names the frames is per-row metadata, and the assembled
|
|
764
|
+
* `Message[]` omits row metadata, so the ids are read back from the rows and
|
|
765
|
+
* matched against the attachment id each image block carries: `workspace_ref`
|
|
766
|
+
* on a reloaded block, `_attachmentId` on the live copy a turn pushed. One
|
|
767
|
+
* uninterrupted call and a reloaded conversation therefore trim from the same
|
|
768
|
+
* pool. A conversation with no tagged rows returns the input array unchanged.
|
|
769
|
+
*
|
|
770
|
+
* Called twice over a turn that compacts: once before the run, and again on the
|
|
771
|
+
* rebuilt history the agent loop installs, since compaction reads the stored
|
|
772
|
+
* rows and can retain frames this pass had already stubbed.
|
|
773
|
+
*
|
|
774
|
+
* Best-effort: this trims cost, it does not decide what the turn means, so a
|
|
775
|
+
* failed read degrades to the untrimmed history rather than failing the turn.
|
|
776
|
+
*/
|
|
777
|
+
export function applySightFrameRetention(
|
|
778
|
+
messages: Message[],
|
|
779
|
+
conversationId: string,
|
|
780
|
+
): Message[] {
|
|
781
|
+
// A frame always traces back to an attachment row, so a history holding no
|
|
782
|
+
// attributable image can be answered without reading any rows. That is the
|
|
783
|
+
// overwhelming majority of turns, and this pass sits on the per-turn critical
|
|
784
|
+
// path.
|
|
785
|
+
if (!hasAttributableImages(messages)) {
|
|
786
|
+
return messages;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
const captureTimes = selectSightFrameCaptureTimes(conversationId);
|
|
790
|
+
if (captureTimes.size === 0) {
|
|
791
|
+
return messages;
|
|
792
|
+
}
|
|
793
|
+
return stripAgedSightFrames(
|
|
794
|
+
messages,
|
|
795
|
+
captureTimes,
|
|
796
|
+
resolveSightKeepLatestFrames(),
|
|
797
|
+
).messages;
|
|
798
|
+
} catch (err) {
|
|
799
|
+
log.warn(
|
|
800
|
+
{ conversationId, err },
|
|
801
|
+
"Camera-frame retention failed (non-fatal)",
|
|
802
|
+
);
|
|
803
|
+
return messages;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
740
807
|
/**
|
|
741
808
|
* Build the `<channel_capabilities>` block text for the given capabilities, or
|
|
742
809
|
* `null` on the happy path (desktop, full capabilities, no special context)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention for ambient camera frames, the images a call samples on its own
|
|
3
|
+
* while the camera is up.
|
|
4
|
+
*
|
|
5
|
+
* Frames arrive every few seconds for as long as the camera is up, and history
|
|
6
|
+
* resends every inline image on every later request, so a long call grows its
|
|
7
|
+
* own context until the provider rejects it. {@link stripAgedSightFrames} keeps
|
|
8
|
+
* only the newest `sight.keepLatestFrames` as real images and replaces the rest
|
|
9
|
+
* with text stubs, on the copy of the history a turn is about to send.
|
|
10
|
+
*
|
|
11
|
+
* This is the proactive counterpart to `stripMediaPayloadsForRetry`
|
|
12
|
+
* (`conversation-media-retry.ts`), which fires only after a provider has
|
|
13
|
+
* already rejected a request and which governs every kind of media. The two
|
|
14
|
+
* live apart because that module reaches the compaction layer for its
|
|
15
|
+
* summary-message predicate and this one must stay a leaf the assembly path can
|
|
16
|
+
* import. They agree on the shape of what they leave behind: a `text` block
|
|
17
|
+
* naming the media that was dropped, so a frame stubbed here is plain text by
|
|
18
|
+
* the time the retry path walks the history and is neither counted as media nor
|
|
19
|
+
* stubbed a second time. Both describe that media through
|
|
20
|
+
* `mediaSourceDescriptor`, which is what holds the two wordings together
|
|
21
|
+
* without either module importing the other.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getConfig } from "../config/loader.js";
|
|
25
|
+
import {
|
|
26
|
+
KEEP_LATEST_SIGHT_FRAMES,
|
|
27
|
+
MAX_SIGHT_KEEP_LATEST_FRAMES,
|
|
28
|
+
MIN_SIGHT_KEEP_LATEST_FRAMES,
|
|
29
|
+
} from "../config/schemas/sight.js";
|
|
30
|
+
import { mediaSourceDescriptor } from "../providers/media-resolve.js";
|
|
31
|
+
import {
|
|
32
|
+
type ContentBlock,
|
|
33
|
+
mediaBlockAttachmentId,
|
|
34
|
+
type Message,
|
|
35
|
+
} from "../providers/types.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The workspace's `sight.keepLatestFrames`, clamped to
|
|
39
|
+
* [{@link MIN_SIGHT_KEEP_LATEST_FRAMES}, {@link MAX_SIGHT_KEEP_LATEST_FRAMES}].
|
|
40
|
+
*
|
|
41
|
+
* Clamped at read rather than rejected by the schema because an out-of-range
|
|
42
|
+
* value states an intent the guardrail can honor part of the way: capping it
|
|
43
|
+
* shows the most frames the budget allows, while failing validation would fall
|
|
44
|
+
* back to the default and show a number neither the config nor the guardrail
|
|
45
|
+
* asked for.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSightKeepLatestFrames(): number {
|
|
48
|
+
const configured = getConfig().sight.keepLatestFrames;
|
|
49
|
+
if (!Number.isFinite(configured)) {
|
|
50
|
+
return KEEP_LATEST_SIGHT_FRAMES;
|
|
51
|
+
}
|
|
52
|
+
return Math.min(
|
|
53
|
+
MAX_SIGHT_KEEP_LATEST_FRAMES,
|
|
54
|
+
Math.max(MIN_SIGHT_KEEP_LATEST_FRAMES, Math.trunc(configured)),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* True when any image in the history can be traced back to an attachment row.
|
|
60
|
+
*
|
|
61
|
+
* The retention pass reads rows to learn which attachments were camera frames;
|
|
62
|
+
* a history holding no attributable image has nothing those rows could match,
|
|
63
|
+
* so callers use this to skip the read entirely. Shares
|
|
64
|
+
* {@link mediaBlockAttachmentId} with the matcher so the two cannot disagree
|
|
65
|
+
* about what is skippable.
|
|
66
|
+
*/
|
|
67
|
+
export function hasAttributableImages(messages: Message[]): boolean {
|
|
68
|
+
return messages.some((message) =>
|
|
69
|
+
message.content.some(
|
|
70
|
+
(block) =>
|
|
71
|
+
block.type === "image" && mediaBlockAttachmentId(block) !== undefined,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Replace every camera frame in `messages` except the newest
|
|
78
|
+
* `keepLatestFrames` with a text stub.
|
|
79
|
+
*
|
|
80
|
+
* `keepLatestFrames` defaults to {@link KEEP_LATEST_SIGHT_FRAMES}, the config
|
|
81
|
+
* default. The assembly path passes {@link resolveSightKeepLatestFrames}
|
|
82
|
+
* instead, so what a turn sends follows the workspace's own setting.
|
|
83
|
+
*
|
|
84
|
+
* `frameCaptureTimes` maps an attachment id the conversation tagged as a camera
|
|
85
|
+
* frame to when the row carrying it was written; an image block counts as a
|
|
86
|
+
* frame only when {@link mediaBlockAttachmentId} resolves it to one of those
|
|
87
|
+
* ids. Every other attachment (picked files, pasted images, shutter photos) is
|
|
88
|
+
* left alone here.
|
|
89
|
+
*
|
|
90
|
+
* Frames are ranked by capture time, not by where they sit in the history:
|
|
91
|
+
* compaction rebuilds the frames it keeps into one synthetic message in the
|
|
92
|
+
* order the model listed them, so position stops tracking recency the moment a
|
|
93
|
+
* conversation has been compacted. The times come from the rows themselves
|
|
94
|
+
* (`createdAt`, which `monotonicNow` keeps strictly increasing), so they order
|
|
95
|
+
* frames the way they were captured and no client-supplied timestamp can perturb
|
|
96
|
+
* that. Frames sharing a row share a time and fall back to history order, which
|
|
97
|
+
* is the order they were attached. Returns the input array itself when nothing
|
|
98
|
+
* is replaced.
|
|
99
|
+
*/
|
|
100
|
+
export function stripAgedSightFrames(
|
|
101
|
+
messages: Message[],
|
|
102
|
+
frameCaptureTimes: ReadonlyMap<string, number>,
|
|
103
|
+
keepLatestFrames: number = KEEP_LATEST_SIGHT_FRAMES,
|
|
104
|
+
): { messages: Message[]; modified: boolean; replacedBlocks: number } {
|
|
105
|
+
if (frameCaptureTimes.size === 0) {
|
|
106
|
+
return { messages, modified: false, replacedBlocks: 0 };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const frames: Array<{
|
|
110
|
+
messageIndex: number;
|
|
111
|
+
blockIndex: number;
|
|
112
|
+
capturedAt: number;
|
|
113
|
+
}> = [];
|
|
114
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
115
|
+
for (const [blockIndex, block] of message.content.entries()) {
|
|
116
|
+
if (block.type !== "image") {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const attachmentId = mediaBlockAttachmentId(block);
|
|
120
|
+
if (attachmentId === undefined) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const capturedAt = frameCaptureTimes.get(attachmentId);
|
|
124
|
+
if (capturedAt === undefined) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
frames.push({ messageIndex, blockIndex, capturedAt });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (frames.length <= keepLatestFrames) {
|
|
131
|
+
return { messages, modified: false, replacedBlocks: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Oldest first by capture time, so dropping the tail keeps the newest. The
|
|
135
|
+
// sort is stable and `frames` was collected in history order, which is what
|
|
136
|
+
// separates frames that share a row's timestamp.
|
|
137
|
+
const byCaptureTime = [...frames].sort((a, b) => a.capturedAt - b.capturedAt);
|
|
138
|
+
const aged = new Map<number, Map<number, number>>();
|
|
139
|
+
for (const frame of byCaptureTime.slice(
|
|
140
|
+
0,
|
|
141
|
+
byCaptureTime.length - keepLatestFrames,
|
|
142
|
+
)) {
|
|
143
|
+
const blocks = aged.get(frame.messageIndex) ?? new Map<number, number>();
|
|
144
|
+
blocks.set(frame.blockIndex, frame.capturedAt);
|
|
145
|
+
aged.set(frame.messageIndex, blocks);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let replacedBlocks = 0;
|
|
149
|
+
const nextMessages = messages.map((message, messageIndex) => {
|
|
150
|
+
const blocks = aged.get(messageIndex);
|
|
151
|
+
if (!blocks) {
|
|
152
|
+
return message;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...message,
|
|
156
|
+
content: message.content.map((block, blockIndex) => {
|
|
157
|
+
const capturedAt = blocks.get(blockIndex);
|
|
158
|
+
if (capturedAt === undefined || block.type !== "image") {
|
|
159
|
+
return block;
|
|
160
|
+
}
|
|
161
|
+
replacedBlocks += 1;
|
|
162
|
+
return sightFrameBlockToStub(block, capturedAt);
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return { messages: nextMessages, modified: true, replacedBlocks };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Stub for a frame that has aged out of the context. Describes the dropped
|
|
172
|
+
* media through `mediaSourceDescriptor`, the same way the retry path's stubs
|
|
173
|
+
* do, and adds the capture time, which is the only thing telling one ambient
|
|
174
|
+
* frame from another once the pixels are gone. A frame is always an image, so
|
|
175
|
+
* the extracted-text folding the retry path does for file blocks has nothing to
|
|
176
|
+
* act on here.
|
|
177
|
+
*/
|
|
178
|
+
function sightFrameBlockToStub(
|
|
179
|
+
block: Extract<ContentBlock, { type: "image" }>,
|
|
180
|
+
capturedAt: number,
|
|
181
|
+
): Extract<ContentBlock, { type: "text" }> {
|
|
182
|
+
const capturedAtIso = new Date(capturedAt).toISOString();
|
|
183
|
+
return {
|
|
184
|
+
type: "text",
|
|
185
|
+
text: `[Camera frame omitted from context: captured ${capturedAtIso}, ${mediaSourceDescriptor(block.source)}]`,
|
|
186
|
+
};
|
|
187
|
+
}
|