@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
package/src/oauth/connection.ts
CHANGED
|
@@ -5,8 +5,9 @@ export interface OAuthConnectionRequest {
|
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
6
|
/**
|
|
7
7
|
* A string is forwarded to the provider verbatim under the caller's own
|
|
8
|
-
* `Content-Type` (multipart, XML, form-encoded).
|
|
9
|
-
* JSON-serialized and sent
|
|
8
|
+
* `Content-Type` (multipart, XML, form-encoded). A Buffer or Uint8Array
|
|
9
|
+
* is forwarded as raw bytes. Anything else is JSON-serialized and sent
|
|
10
|
+
* as `application/json`.
|
|
10
11
|
*/
|
|
11
12
|
body?: unknown;
|
|
12
13
|
/**
|
|
@@ -102,6 +102,41 @@ describe("PlatformOAuthConnection", () => {
|
|
|
102
102
|
expect(result.body).toEqual(upstreamBody);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
test("encodes a Buffer request body as base64 in the proxy envelope", async () => {
|
|
106
|
+
const binary = Buffer.from([
|
|
107
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe, 0x00,
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const client = makeMockClient(
|
|
111
|
+
mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
112
|
+
const parsed = JSON.parse(init?.body as string);
|
|
113
|
+
expect(parsed.request.body).toBe(binary.toString("base64"));
|
|
114
|
+
expect(parsed.request.body_encoding).toBe("base64");
|
|
115
|
+
expect(parsed.request.headers["Content-Type"]).toBe("application/pdf");
|
|
116
|
+
|
|
117
|
+
return new Response(
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
status: 200,
|
|
120
|
+
headers: { "content-type": "application/json" },
|
|
121
|
+
body: { id: "file-123" },
|
|
122
|
+
}),
|
|
123
|
+
{ status: 200 },
|
|
124
|
+
);
|
|
125
|
+
}) as unknown as typeof globalThis.fetch,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const conn = new PlatformOAuthConnection({
|
|
129
|
+
...DEFAULT_OPTIONS,
|
|
130
|
+
client,
|
|
131
|
+
});
|
|
132
|
+
await conn.request({
|
|
133
|
+
method: "POST",
|
|
134
|
+
path: "/upload/drive/v3/files",
|
|
135
|
+
headers: { "Content-Type": "application/pdf" },
|
|
136
|
+
body: binary,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
105
140
|
test("decodes base64 binary proxy bodies into a Buffer", async () => {
|
|
106
141
|
const binary = Buffer.from([
|
|
107
142
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe, 0x00,
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
OAuthConnectionRequest,
|
|
8
8
|
OAuthConnectionResponse,
|
|
9
9
|
} from "./connection.js";
|
|
10
|
+
import { isBinaryOAuthBody } from "./connection.js";
|
|
10
11
|
|
|
11
12
|
const log = getLogger("platform-oauth-connection");
|
|
12
13
|
const MAX_RETRIES = 3;
|
|
@@ -84,20 +85,24 @@ export class PlatformOAuthConnection implements OAuthConnection {
|
|
|
84
85
|
|
|
85
86
|
// The envelope carries the caller's headers and body side by side. A
|
|
86
87
|
// string body is placed in the envelope as a string, so the proxy forwards
|
|
87
|
-
// those bytes verbatim under the caller's Content-Type
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
},
|
|
88
|
+
// those bytes verbatim under the caller's Content-Type. A Buffer is
|
|
89
|
+
// base64-encoded so binary uploads survive JSON. An object body travels
|
|
90
|
+
// as JSON and the proxy serializes it.
|
|
91
|
+
const request: Record<string, unknown> = {
|
|
92
|
+
method: req.method,
|
|
93
|
+
path: req.path,
|
|
94
|
+
query: req.query ?? {},
|
|
95
|
+
headers: req.headers ?? {},
|
|
96
|
+
body: req.body ?? null,
|
|
97
|
+
...((req.baseUrl ?? this.baseUrl)
|
|
98
|
+
? { base_url: req.baseUrl ?? this.baseUrl }
|
|
99
|
+
: {}),
|
|
100
100
|
};
|
|
101
|
+
if (isBinaryOAuthBody(req.body)) {
|
|
102
|
+
request.body = Buffer.from(req.body).toString("base64");
|
|
103
|
+
request.body_encoding = "base64";
|
|
104
|
+
}
|
|
105
|
+
const body: Record<string, unknown> = { request };
|
|
101
106
|
|
|
102
107
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
103
108
|
const response = await this.client.fetch(proxyPath, {
|
package/src/oauth/scope-utils.ts
CHANGED
|
@@ -24,6 +24,54 @@ export function scopeDifference(
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Split a scope list the way the provider writes it.
|
|
29
|
+
*
|
|
30
|
+
* Providers (GitHub, Slack) may return comma-separated scopes regardless of
|
|
31
|
+
* the separator used to join outbound authorize URLs, so the default tolerates
|
|
32
|
+
* both spaces and commas. A provider that explicitly configures a non-default
|
|
33
|
+
* separator (Linear uses ",") is honored, keeping configured scopes
|
|
34
|
+
* round-tripping symmetrically.
|
|
35
|
+
*
|
|
36
|
+
* The one parser for both directions: token responses and authorize
|
|
37
|
+
* parameters. Two copies would let a provider-format fix land on one and not
|
|
38
|
+
* the other.
|
|
39
|
+
*/
|
|
40
|
+
export function parseScopeList(
|
|
41
|
+
raw: string,
|
|
42
|
+
scopeSeparator: string = " ",
|
|
43
|
+
): string[] {
|
|
44
|
+
const splitPattern = scopeSeparator === " " ? /[ ,]/ : scopeSeparator;
|
|
45
|
+
return raw
|
|
46
|
+
.split(splitPattern)
|
|
47
|
+
.map((scope) => scope.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The scopes the stored access token is expected to carry.
|
|
53
|
+
*
|
|
54
|
+
* A provider that asks for user scopes alongside bot scopes issues two grants
|
|
55
|
+
* from one authorization. `exchangeCodeForTokens` stores the `authed_user`
|
|
56
|
+
* token in that case and records that token's own grant as `grantedScopes`, so
|
|
57
|
+
* coverage has to be measured against the request that produced the stored
|
|
58
|
+
* token: `user_scope` where the provider sends one, the `scope` parameter
|
|
59
|
+
* otherwise. Measuring a user grant against the bot request reports scopes the
|
|
60
|
+
* authorization could never place on that token.
|
|
61
|
+
*/
|
|
62
|
+
export function expectedScopesForStoredToken(
|
|
63
|
+
defaultScopes: string[],
|
|
64
|
+
authorizeParams: Record<string, string> | undefined,
|
|
65
|
+
scopeSeparator?: string,
|
|
66
|
+
): string[] {
|
|
67
|
+
const userScope = authorizeParams?.user_scope;
|
|
68
|
+
if (typeof userScope !== "string") {
|
|
69
|
+
return defaultScopes;
|
|
70
|
+
}
|
|
71
|
+
const parsed = parseScopeList(userScope, scopeSeparator);
|
|
72
|
+
return parsed.length > 0 ? parsed : defaultScopes;
|
|
73
|
+
}
|
|
74
|
+
|
|
27
75
|
const GMAIL_FULL_ACCESS_SCOPE = "https://mail.google.com/";
|
|
28
76
|
const GMAIL_READONLY_SCOPE = "https://www.googleapis.com/auth/gmail.readonly";
|
|
29
77
|
|
|
@@ -183,6 +183,14 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
183
183
|
clientIdPlaceholder: null,
|
|
184
184
|
logoUrl:
|
|
185
185
|
"https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/slack/default.svg",
|
|
186
|
+
// Sent as the bot `scope` parameter, while this flow persists the
|
|
187
|
+
// `authed_user` token and stores `authed_user.scope` as `grantedScopes`.
|
|
188
|
+
// Credential health compares this list against that grant, so a scope
|
|
189
|
+
// listed here that `user_scope` does not also request reports as
|
|
190
|
+
// `missing_scopes`, a hard failure that disables the provider's tools.
|
|
191
|
+
// The wider bot set belongs to Socket Mode installs, which verify the live
|
|
192
|
+
// `x-oauth-scopes` header against SLACK_REQUIRED_BOT_SCOPES in
|
|
193
|
+
// channel-readiness-service.ts.
|
|
186
194
|
defaultScopes: [
|
|
187
195
|
"channels:join",
|
|
188
196
|
"channels:read",
|
|
@@ -200,6 +208,10 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
200
208
|
"reactions:write",
|
|
201
209
|
],
|
|
202
210
|
availableScopes: "https://api.slack.com/scopes",
|
|
211
|
+
// Carries write scopes because the persisted `authed_user` token is the
|
|
212
|
+
// only credential this flow stores, so it acts as the installer for every
|
|
213
|
+
// call including `chat.postMessage`. The manifest's user list is read-only
|
|
214
|
+
// instead, since a Socket Mode install keeps a bot token beside it.
|
|
203
215
|
authorizeParams: {
|
|
204
216
|
user_scope:
|
|
205
217
|
"channels:read,channels:history,groups:read,groups:history,im:read,im:history,im:write,mpim:read,mpim:history,users:read,chat:write,search:read,reactions:write",
|
|
@@ -49,6 +49,7 @@ export async function ensureLocalCA(dataDir: string): Promise<void> {
|
|
|
49
49
|
const keyProc = Bun.spawn(["openssl", "genrsa", "-out", keyPath, "2048"], {
|
|
50
50
|
stdout: "pipe",
|
|
51
51
|
stderr: "pipe",
|
|
52
|
+
windowsHide: true,
|
|
52
53
|
});
|
|
53
54
|
const keyExit = await keyProc.exited;
|
|
54
55
|
if (keyExit !== 0) {
|
|
@@ -76,7 +77,7 @@ export async function ensureLocalCA(dataDir: string): Promise<void> {
|
|
|
76
77
|
"-addext",
|
|
77
78
|
"keyUsage=critical,keyCertSign,cRLSign",
|
|
78
79
|
],
|
|
79
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
80
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
80
81
|
);
|
|
81
82
|
const certExit = await certProc.exited;
|
|
82
83
|
if (certExit !== 0) {
|
|
@@ -144,7 +145,7 @@ export async function issueLeafCert(
|
|
|
144
145
|
// Generate leaf key
|
|
145
146
|
const keyProc = Bun.spawn(
|
|
146
147
|
["openssl", "genrsa", "-out", leafKeyPath, "2048"],
|
|
147
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
148
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
148
149
|
);
|
|
149
150
|
const keyExit = await keyProc.exited;
|
|
150
151
|
if (keyExit !== 0) {
|
|
@@ -166,7 +167,7 @@ export async function issueLeafCert(
|
|
|
166
167
|
"-subj",
|
|
167
168
|
`/CN=${hostname}`,
|
|
168
169
|
],
|
|
169
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
170
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
170
171
|
);
|
|
171
172
|
const csrExit = await csrProc.exited;
|
|
172
173
|
if (csrExit !== 0) {
|
|
@@ -198,7 +199,7 @@ export async function issueLeafCert(
|
|
|
198
199
|
"-extfile",
|
|
199
200
|
extPath,
|
|
200
201
|
],
|
|
201
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
202
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
202
203
|
);
|
|
203
204
|
const signExit = await signProc.exited;
|
|
204
205
|
if (signExit !== 0) {
|
|
@@ -21,7 +21,7 @@ const bridgeMock = mock((_params: Record<string, unknown>) =>
|
|
|
21
21
|
Promise.resolve({ bridged: true as const, signalId: "req-1" }),
|
|
22
22
|
);
|
|
23
23
|
const withdrawCardsMock = mock((_params: Record<string, unknown>) =>
|
|
24
|
-
Promise.resolve(),
|
|
24
|
+
Promise.resolve({ complete: true }),
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
let trustContext: TrustContext | undefined;
|
|
@@ -37,7 +37,13 @@ mock.module("../daemon/conversation-registry.js", () => ({
|
|
|
37
37
|
})
|
|
38
38
|
: undefined,
|
|
39
39
|
}));
|
|
40
|
+
// Spread the actual module so transitive importers of names this
|
|
41
|
+
// factory does not stub keep resolving (a partial factory breaks at
|
|
42
|
+
// import time when the graph gains a new named import).
|
|
43
|
+
const actualGatewayGuardianRequests =
|
|
44
|
+
await import("../channels/gateway-guardian-requests.js");
|
|
40
45
|
mock.module("../channels/gateway-guardian-requests.js", () => ({
|
|
46
|
+
...actualGatewayGuardianRequests,
|
|
41
47
|
createGuardianRequest: (params: Record<string, unknown>) =>
|
|
42
48
|
createGuardianRequestMock(params),
|
|
43
49
|
expireGuardianRequest: (id: string) => expireGuardianRequestMock(id),
|
|
@@ -273,36 +273,52 @@ function materializeAttachmentIntoConversation(
|
|
|
273
273
|
const resolvedName = resolveUniqueFilename(attachDir, row.originalFilename);
|
|
274
274
|
const targetPath = join(attachDir, resolvedName);
|
|
275
275
|
|
|
276
|
-
let sourcePath = row.sourcePath;
|
|
277
|
-
if (row.dataBase64) {
|
|
278
|
-
writeFileSync(targetPath, Buffer.from(row.dataBase64, "base64"));
|
|
279
|
-
} else {
|
|
280
|
-
const readablePath = [row.filePath, row.sourcePath].find(
|
|
281
|
-
(path): path is string => !!path && existsSync(path),
|
|
282
|
-
);
|
|
283
|
-
if (!readablePath) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (!sourcePath && readablePath !== row.filePath) {
|
|
288
|
-
sourcePath = readablePath;
|
|
289
|
-
} else if (
|
|
290
|
-
!sourcePath &&
|
|
291
|
-
readablePath === row.filePath &&
|
|
292
|
-
dirname(readablePath) !== attachDir
|
|
293
|
-
) {
|
|
294
|
-
sourcePath = readablePath;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
copyFileSync(readablePath, targetPath);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
276
|
// Remember the old file path before updating the DB row, so we can
|
|
301
277
|
// clean up the staging copy (e.g. in data/attachments/) after the
|
|
302
278
|
// canonical path moves to the conversation directory.
|
|
303
279
|
const previousFilePath = row.filePath;
|
|
304
280
|
|
|
305
|
-
|
|
281
|
+
let sourcePath = row.sourcePath;
|
|
282
|
+
try {
|
|
283
|
+
if (row.dataBase64) {
|
|
284
|
+
writeFileSync(targetPath, Buffer.from(row.dataBase64, "base64"));
|
|
285
|
+
} else {
|
|
286
|
+
const readablePath = [row.filePath, row.sourcePath].find(
|
|
287
|
+
(path): path is string => !!path && existsSync(path),
|
|
288
|
+
);
|
|
289
|
+
if (!readablePath) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!sourcePath && readablePath !== row.filePath) {
|
|
294
|
+
sourcePath = readablePath;
|
|
295
|
+
} else if (
|
|
296
|
+
!sourcePath &&
|
|
297
|
+
readablePath === row.filePath &&
|
|
298
|
+
dirname(readablePath) !== attachDir
|
|
299
|
+
) {
|
|
300
|
+
sourcePath = readablePath;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
copyFileSync(readablePath, targetPath);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
persistAttachmentFilePath(row.id, targetPath, sourcePath);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
// Only the copy this call was making. `resolveUniqueFilename` picked a name
|
|
309
|
+
// nothing occupied, so whatever sits at `targetPath` now is this attempt's
|
|
310
|
+
// own work. The row's `filePath` is deliberately left alone: until the
|
|
311
|
+
// update above lands it can still name the file the bytes came FROM, and
|
|
312
|
+
// for a fresh clone that file belongs to another conversation.
|
|
313
|
+
if (existsSync(targetPath)) {
|
|
314
|
+
try {
|
|
315
|
+
unlinkSync(targetPath);
|
|
316
|
+
} catch {
|
|
317
|
+
/* leave the copy rather than fail the failure */
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
306
322
|
|
|
307
323
|
// Remove the old staging file now that the canonical copy lives in
|
|
308
324
|
// the conversation directory. Only delete files that live in the
|
|
@@ -344,18 +360,58 @@ function scopeAttachmentToConversation(
|
|
|
344
360
|
}
|
|
345
361
|
|
|
346
362
|
const linkedConversationIds = listLinkedConversationIds(attachmentId);
|
|
347
|
-
|
|
363
|
+
const cloned = linkedConversationIds.some((id) => id !== conversationId);
|
|
364
|
+
if (cloned) {
|
|
348
365
|
row = cloneAttachmentRow(row);
|
|
349
366
|
}
|
|
350
367
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
368
|
+
try {
|
|
369
|
+
materializeAttachmentIntoConversation(
|
|
370
|
+
row,
|
|
371
|
+
conversationId,
|
|
372
|
+
conversationCreatedAt,
|
|
373
|
+
);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (cloned) {
|
|
376
|
+
discardFailedCloneRow(row.id);
|
|
377
|
+
}
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
356
380
|
return row.id;
|
|
357
381
|
}
|
|
358
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Drop the row a failed clone left behind, touching no file at all.
|
|
385
|
+
*
|
|
386
|
+
* A clone is inserted carrying the SOURCE row's `filePath`, and only a
|
|
387
|
+
* successful materialization repoints it at a copy of its own. At the moment
|
|
388
|
+
* materialization fails the row can therefore still name the original's file,
|
|
389
|
+
* and deleting that would take the source conversation's data with it. This is
|
|
390
|
+
* the hazard the staging-dir guard in
|
|
391
|
+
* {@link materializeAttachmentIntoConversation} exists for, applied to the
|
|
392
|
+
* other end of the same window.
|
|
393
|
+
*
|
|
394
|
+
* The file this attempt was creating is cleaned up by materialization itself,
|
|
395
|
+
* which is the only code that knows the path it chose.
|
|
396
|
+
*
|
|
397
|
+
* Best effort. Rethrowing the original failure matters more than the row, and
|
|
398
|
+
* the caller converts that failure into an inline fallback either way.
|
|
399
|
+
*/
|
|
400
|
+
function discardFailedCloneRow(clonedId: string): void {
|
|
401
|
+
try {
|
|
402
|
+
rawRun(
|
|
403
|
+
"attachments:discardFailedCloneRow",
|
|
404
|
+
`DELETE FROM attachments WHERE id = ?`,
|
|
405
|
+
clonedId,
|
|
406
|
+
);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
getLogger("attachments-store").warn(
|
|
409
|
+
{ err, attachmentId: clonedId },
|
|
410
|
+
"Could not discard the row left by a failed attachment clone",
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
359
415
|
/**
|
|
360
416
|
* Scope a pre-uploaded attachment into a conversation WITHOUT linking it to a
|
|
361
417
|
* message, returning the scoped row's metadata. Mirrors {@link createInlineAttachment}
|
|
@@ -1164,6 +1220,32 @@ export function getAttachmentsByIds(
|
|
|
1164
1220
|
return results;
|
|
1165
1221
|
}
|
|
1166
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* Hydrate attachment ids into the shape a persisted user message stores:
|
|
1225
|
+
* base64 data plus the on-disk source path where one is known. Ids with no
|
|
1226
|
+
* attachment row are dropped, so a caller that needs to know an id was bad
|
|
1227
|
+
* compares the returned length against what it asked for.
|
|
1228
|
+
*/
|
|
1229
|
+
export function resolveAttachmentsForPersist(attachmentIds: string[]): Array<{
|
|
1230
|
+
id: string;
|
|
1231
|
+
filename: string;
|
|
1232
|
+
mimeType: string;
|
|
1233
|
+
data: string;
|
|
1234
|
+
filePath?: string;
|
|
1235
|
+
}> {
|
|
1236
|
+
const resolved = getAttachmentsByIds(attachmentIds, {
|
|
1237
|
+
hydrateFileData: true,
|
|
1238
|
+
});
|
|
1239
|
+
const sourcePaths = getSourcePathsForAttachments(attachmentIds);
|
|
1240
|
+
return resolved.map((a) => ({
|
|
1241
|
+
id: a.id,
|
|
1242
|
+
filename: a.originalFilename,
|
|
1243
|
+
mimeType: a.mimeType,
|
|
1244
|
+
data: a.dataBase64,
|
|
1245
|
+
...(sourcePaths.has(a.id) ? { filePath: sourcePaths.get(a.id) } : {}),
|
|
1246
|
+
}));
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1167
1249
|
export function linkAttachmentToMessage(
|
|
1168
1250
|
messageId: string,
|
|
1169
1251
|
attachmentId: string,
|
|
@@ -1332,6 +1414,16 @@ export function deleteOrphanAttachments(candidateIds: string[]): number {
|
|
|
1332
1414
|
|
|
1333
1415
|
// Clean up on-disk files only after the DB rows have been removed
|
|
1334
1416
|
for (const filePath of orphanFilePaths) {
|
|
1417
|
+
// A path is not private to one row. A clone carries the source's
|
|
1418
|
+
// `filePath` until materialization repoints it, and an attachment already
|
|
1419
|
+
// sitting in the destination's directory is scoped without being copied at
|
|
1420
|
+
// all, so a surviving row can still name this file. Unlinking it then
|
|
1421
|
+
// destroys another conversation's data, which is what the staging-dir
|
|
1422
|
+
// guard in `materializeAttachmentIntoConversation` avoids at the other end
|
|
1423
|
+
// of the same window.
|
|
1424
|
+
if (attachmentFilePathStillReferenced(filePath)) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1335
1427
|
try {
|
|
1336
1428
|
unlinkSync(filePath);
|
|
1337
1429
|
} catch {
|
|
@@ -1341,3 +1433,14 @@ export function deleteOrphanAttachments(candidateIds: string[]): number {
|
|
|
1341
1433
|
|
|
1342
1434
|
return deletedCount;
|
|
1343
1435
|
}
|
|
1436
|
+
|
|
1437
|
+
/** True while any surviving attachment row still names this file. */
|
|
1438
|
+
function attachmentFilePathStillReferenced(filePath: string): boolean {
|
|
1439
|
+
return (
|
|
1440
|
+
rawGet<{ id: string }>(
|
|
1441
|
+
"attachments:filePathStillReferenced",
|
|
1442
|
+
`SELECT id FROM attachments WHERE file_path = ? LIMIT 1`,
|
|
1443
|
+
filePath,
|
|
1444
|
+
) !== null
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
@@ -91,8 +91,12 @@ import {
|
|
|
91
91
|
type ConversationCreateType,
|
|
92
92
|
type ConversationOrigin,
|
|
93
93
|
isHiddenMessageMetadata,
|
|
94
|
+
isNoResponseMetadata,
|
|
95
|
+
isReactionMessageMetadata,
|
|
94
96
|
isSystemCardMetadata,
|
|
95
97
|
PINNED_GROUP_ID,
|
|
98
|
+
SIGHT_FRAME_ATTACHMENT_IDS_KEY,
|
|
99
|
+
sightFrameAttachmentIdsFromMetadata,
|
|
96
100
|
UNGROUPED_GROUP_ID,
|
|
97
101
|
} from "./conversation-types.js";
|
|
98
102
|
import { runAsyncSqlite } from "./db-async-query.js";
|
|
@@ -458,7 +462,10 @@ export {
|
|
|
458
462
|
* alongside the schema that carries them.
|
|
459
463
|
*/
|
|
460
464
|
export {
|
|
465
|
+
isNoResponseMetadata,
|
|
461
466
|
isSystemCardMetadata,
|
|
467
|
+
NO_RESPONSE_MESSAGE_KIND,
|
|
468
|
+
REACTION_MESSAGE_KIND,
|
|
462
469
|
SYSTEM_CARD_MESSAGE_KIND,
|
|
463
470
|
} from "./conversation-types.js";
|
|
464
471
|
|
|
@@ -485,7 +492,7 @@ export function isProviderErrorMetadata(
|
|
|
485
492
|
|
|
486
493
|
/**
|
|
487
494
|
* True when an assistant row is a standalone display turn: a system card or
|
|
488
|
-
* a provider-error notice. Standalone rows never merge with adjacent
|
|
495
|
+
* a provider-error notice, or a deliberate-silence marker. Standalone rows never merge with adjacent
|
|
489
496
|
* assistant rows, and turn grouping closes on them, so display merging and
|
|
490
497
|
* the turn resolver agree on boundaries. Takes the raw persisted `metadata`
|
|
491
498
|
* JSON string; malformed JSON and non-assistant roles are never standalone.
|
|
@@ -499,7 +506,12 @@ export function isStandaloneAssistantMessage(
|
|
|
499
506
|
}
|
|
500
507
|
try {
|
|
501
508
|
const parsed = JSON.parse(metadata) as Record<string, unknown>;
|
|
502
|
-
return
|
|
509
|
+
return (
|
|
510
|
+
isSystemCardMetadata(parsed) ||
|
|
511
|
+
isProviderErrorMetadata(parsed) ||
|
|
512
|
+
isNoResponseMetadata(parsed) ||
|
|
513
|
+
isReactionMessageMetadata(parsed)
|
|
514
|
+
);
|
|
503
515
|
} catch {
|
|
504
516
|
return false;
|
|
505
517
|
}
|
|
@@ -1675,6 +1687,8 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
|
|
|
1675
1687
|
});
|
|
1676
1688
|
}
|
|
1677
1689
|
|
|
1690
|
+
widenForkSightFrameTags(messagesToCopy, forkedMessageIds, attachmentIdMap);
|
|
1691
|
+
|
|
1678
1692
|
// Set lastMessageAt to the max createdAt of copied messages so the
|
|
1679
1693
|
// forked conversation sorts correctly by message recency.
|
|
1680
1694
|
const lastCopiedMessage = messagesToCopy.at(-1);
|
|
@@ -1718,6 +1732,75 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
|
|
|
1718
1732
|
}
|
|
1719
1733
|
}
|
|
1720
1734
|
|
|
1735
|
+
/**
|
|
1736
|
+
* Extend the copied rows' camera-frame tags to name the fork's cloned
|
|
1737
|
+
* attachment ids alongside the source ids they were written with.
|
|
1738
|
+
*
|
|
1739
|
+
* A fork leaves its rows describing their attachments two different ways:
|
|
1740
|
+
* `messages.content` is copied byte for byte and still names the SOURCE
|
|
1741
|
+
* attachment ids, while `message_attachments` is re-linked to freshly CLONED
|
|
1742
|
+
* rows under new ids. Readers split along that seam. Camera-frame retention
|
|
1743
|
+
* matches the tag against the ids in the content blocks (source ids), and the
|
|
1744
|
+
* compactor builds its image manifest from the links (cloned ids) and stamps
|
|
1745
|
+
* those onto the frames it rebuilds. A tag naming only one vocabulary goes
|
|
1746
|
+
* blind on the other, so it names both.
|
|
1747
|
+
*
|
|
1748
|
+
* Widening rather than remapping is deliberate: replacing the source ids would
|
|
1749
|
+
* fix the compactor's frames by breaking every frame the fork holds directly,
|
|
1750
|
+
* which is the common case. Extra ids are inert, since an id no block carries
|
|
1751
|
+
* simply never matches.
|
|
1752
|
+
*
|
|
1753
|
+
* Runs after the attachment loop because that loop is what produces the id map.
|
|
1754
|
+
* Only rows that actually carry a tag are rewritten, so an ordinary fork does
|
|
1755
|
+
* no extra writes.
|
|
1756
|
+
*/
|
|
1757
|
+
function widenForkSightFrameTags(
|
|
1758
|
+
messagesToCopy: MessageRow[],
|
|
1759
|
+
forkedMessageIds: Map<string, string>,
|
|
1760
|
+
attachmentIdMap: Map<string, string>,
|
|
1761
|
+
): void {
|
|
1762
|
+
if (attachmentIdMap.size === 0) {
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const db = getDb();
|
|
1766
|
+
for (const message of messagesToCopy) {
|
|
1767
|
+
const forkedMessageId = forkedMessageIds.get(message.id);
|
|
1768
|
+
if (!forkedMessageId) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const sourceMetadata = parseMessageMetadata(message.metadata);
|
|
1772
|
+
const sourceIds = sightFrameAttachmentIdsFromMetadata(sourceMetadata);
|
|
1773
|
+
if (sourceIds.length === 0) {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const widened = new Set(sourceIds);
|
|
1777
|
+
for (const sourceId of sourceIds) {
|
|
1778
|
+
const clonedId = attachmentIdMap.get(sourceId);
|
|
1779
|
+
if (clonedId) {
|
|
1780
|
+
widened.add(clonedId);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (widened.size === sourceIds.length) {
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
const forkedRow = db
|
|
1787
|
+
.select({ metadata: messages.metadata })
|
|
1788
|
+
.from(messages)
|
|
1789
|
+
.where(eq(messages.id, forkedMessageId))
|
|
1790
|
+
.get();
|
|
1791
|
+
const forkedMetadata = parseMessageMetadata(forkedRow?.metadata ?? null);
|
|
1792
|
+
db.update(messages)
|
|
1793
|
+
.set({
|
|
1794
|
+
metadata: JSON.stringify({
|
|
1795
|
+
...(forkedMetadata ?? {}),
|
|
1796
|
+
[SIGHT_FRAME_ATTACHMENT_IDS_KEY]: [...widened],
|
|
1797
|
+
}),
|
|
1798
|
+
})
|
|
1799
|
+
.where(eq(messages.id, forkedMessageId))
|
|
1800
|
+
.run();
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1721
1804
|
/**
|
|
1722
1805
|
* How many leading rows of the inherited window sit behind the source's
|
|
1723
1806
|
* compaction boundary, counted in `getMessages` load order (`createdAt`
|
|
@@ -2494,6 +2577,41 @@ export function selectProviderMetaCandidateMetadata(
|
|
|
2494
2577
|
return out;
|
|
2495
2578
|
}
|
|
2496
2579
|
|
|
2580
|
+
/**
|
|
2581
|
+
* Every attachment the conversation's rows tagged as an ambient camera frame,
|
|
2582
|
+
* mapped to the `createdAt` of the row that carried it.
|
|
2583
|
+
*
|
|
2584
|
+
* Like the Slack prefilter above, the `LIKE` is an indexable narrowing only:
|
|
2585
|
+
* each candidate row is parsed and validated before an id is taken, so a row
|
|
2586
|
+
* that merely mentions the key contributes nothing. Any row state qualifies
|
|
2587
|
+
* because the tag is written with the insert, and the lineage filter is what
|
|
2588
|
+
* lets a fork see the frames it inherited.
|
|
2589
|
+
*/
|
|
2590
|
+
export function selectSightFrameCaptureTimes(
|
|
2591
|
+
conversationId: string,
|
|
2592
|
+
): Map<string, number> {
|
|
2593
|
+
const db = getDb();
|
|
2594
|
+
const rows = db
|
|
2595
|
+
.select({ metadata: messages.metadata, createdAt: messages.createdAt })
|
|
2596
|
+
.from(messages)
|
|
2597
|
+
.where(
|
|
2598
|
+
and(
|
|
2599
|
+
lineageFilter(conversationId),
|
|
2600
|
+
like(messages.metadata, `%"${SIGHT_FRAME_ATTACHMENT_IDS_KEY}"%`),
|
|
2601
|
+
),
|
|
2602
|
+
)
|
|
2603
|
+
.orderBy(asc(messages.createdAt))
|
|
2604
|
+
.all();
|
|
2605
|
+
const captureTimes = new Map<string, number>();
|
|
2606
|
+
for (const row of rows) {
|
|
2607
|
+
const metadata = parseMessageMetadata(row.metadata);
|
|
2608
|
+
for (const id of sightFrameAttachmentIdsFromMetadata(metadata)) {
|
|
2609
|
+
captureTimes.set(id, row.createdAt);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return captureTimes;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2497
2615
|
/**
|
|
2498
2616
|
* Count messages in a conversation that were created strictly after the
|
|
2499
2617
|
* `afterMessageId` reference message. If `afterMessageId` is `null` or empty,
|
|
@@ -4082,8 +4200,18 @@ export function relinkAttachments(
|
|
|
4082
4200
|
*
|
|
4083
4201
|
* Returns segment IDs so the caller can clean up the corresponding
|
|
4084
4202
|
* Qdrant vector entries.
|
|
4203
|
+
*
|
|
4204
|
+
* `retainAttachmentIds` exempts attachments from the orphan cleanup below, for
|
|
4205
|
+
* a caller rolling a message back rather than deleting it: the attachment
|
|
4206
|
+
* returns to the uploaded-but-unlinked state it was in before the row existed,
|
|
4207
|
+
* which is a live state (that is where every attachment sits between upload and
|
|
4208
|
+
* send), not a leak. Callers deleting a message for good pass nothing and the
|
|
4209
|
+
* attachments it alone referenced are collected.
|
|
4085
4210
|
*/
|
|
4086
|
-
export function deleteMessageById(
|
|
4211
|
+
export function deleteMessageById(
|
|
4212
|
+
messageId: string,
|
|
4213
|
+
options?: { retainAttachmentIds?: readonly string[] },
|
|
4214
|
+
): DeletedMemoryIds {
|
|
4087
4215
|
const db = getDb();
|
|
4088
4216
|
const result: DeletedMemoryIds = {
|
|
4089
4217
|
segmentIds: [],
|
|
@@ -4145,7 +4273,12 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
|
4145
4273
|
...purgeMessageSegments([messageId], "deleteMessageById:post-delete"),
|
|
4146
4274
|
);
|
|
4147
4275
|
|
|
4148
|
-
|
|
4276
|
+
const retainAttachmentIds = options?.retainAttachmentIds;
|
|
4277
|
+
deleteOrphanAttachments(
|
|
4278
|
+
retainAttachmentIds && retainAttachmentIds.length > 0
|
|
4279
|
+
? candidateAttachmentIds.filter((id) => !retainAttachmentIds.includes(id))
|
|
4280
|
+
: candidateAttachmentIds,
|
|
4281
|
+
);
|
|
4149
4282
|
|
|
4150
4283
|
// Remove the deleted message's point from the lexical index. Enqueued only
|
|
4151
4284
|
// when the row actually existed (`msgRow` set), after the transaction and
|