@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
|
@@ -157,6 +157,45 @@ export function isSystemCardMetadata(
|
|
|
157
157
|
return metadata?.messageKind === SYSTEM_CARD_MESSAGE_KIND;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Marker for a turn whose whole reply was the `<no_response/>` sentinel: the
|
|
162
|
+
* assistant deliberately chose silence. The row's content keeps the raw
|
|
163
|
+
* sentinel (the model reads its own convention back from history); clients
|
|
164
|
+
* switch on the marker to render a quiet standalone notice and to resolve
|
|
165
|
+
* their pending-response state, instead of showing the sentinel text or
|
|
166
|
+
* waiting forever for a reply that was never coming.
|
|
167
|
+
*/
|
|
168
|
+
export const NO_RESPONSE_MESSAGE_KIND = "no_response";
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Shared predicate for the deliberate-silence marker, mirroring
|
|
172
|
+
* {@link isSystemCardMetadata} so display merging, transcript rendering, and
|
|
173
|
+
* turn grouping cannot drift.
|
|
174
|
+
*/
|
|
175
|
+
export function isNoResponseMetadata(
|
|
176
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
177
|
+
): boolean {
|
|
178
|
+
return metadata?.messageKind === NO_RESPONSE_MESSAGE_KIND;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Marker for an assistant-authored reaction row: the assistant reacted with
|
|
183
|
+
* an emoji, and the row records it durably. The reaction fact itself lives
|
|
184
|
+
* in the row's `providerMeta.reaction` envelope, same as inbound reaction
|
|
185
|
+
* rows; this kind carries the display and grouping semantics, keeping the
|
|
186
|
+
* row a standalone turn (never merged into adjacent assistant speech, where
|
|
187
|
+
* consolidation would fold its sentinel text into a bubble and drop the
|
|
188
|
+
* envelope from the wire).
|
|
189
|
+
*/
|
|
190
|
+
export const REACTION_MESSAGE_KIND = "reaction";
|
|
191
|
+
|
|
192
|
+
/** Shared predicate for the assistant-reaction marker. */
|
|
193
|
+
export function isReactionMessageMetadata(
|
|
194
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
195
|
+
): boolean {
|
|
196
|
+
return metadata?.messageKind === REACTION_MESSAGE_KIND;
|
|
197
|
+
}
|
|
198
|
+
|
|
160
199
|
/**
|
|
161
200
|
* True when a role-`"user"` row is internal scaffolding rather than a person's
|
|
162
201
|
* prompt: a daemon-injected run lifecycle notification (subagent
|
|
@@ -227,6 +266,76 @@ export function isVoiceSessionUserMessage(
|
|
|
227
266
|
return metadata?.voiceSessionTurn === true;
|
|
228
267
|
}
|
|
229
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Metadata key naming which of a row's attachments arrived as ambient camera
|
|
271
|
+
* frames rather than files the user picked. Exported so the SQL prefilter that
|
|
272
|
+
* finds candidate rows and the reader below name the same key.
|
|
273
|
+
*/
|
|
274
|
+
export const SIGHT_FRAME_ATTACHMENT_IDS_KEY = "sightFrameAttachmentIds";
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The row's attachments that arrived as ambient camera frames, by attachment
|
|
278
|
+
* id. A user attachment carries no metadata of its own, so the marking lives on
|
|
279
|
+
* the message and refers to the attachments by id.
|
|
280
|
+
*
|
|
281
|
+
* Tolerant of any metadata shape: a row without the key, or one whose value is
|
|
282
|
+
* not an array of ids, yields nothing. Retention consumers therefore treat an
|
|
283
|
+
* untagged conversation exactly like one that predates the tag.
|
|
284
|
+
*/
|
|
285
|
+
export function sightFrameAttachmentIdsFromMetadata(
|
|
286
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
287
|
+
): string[] {
|
|
288
|
+
const ids = metadata?.[SIGHT_FRAME_ATTACHMENT_IDS_KEY];
|
|
289
|
+
if (!Array.isArray(ids)) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
return ids.filter(
|
|
293
|
+
(id): id is string => typeof id === "string" && id.length > 0,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* True when a stored `messages.metadata` column belongs to a standalone
|
|
299
|
+
* ambient camera keep: a row the call's own gate sampled, which no one spoke.
|
|
300
|
+
*
|
|
301
|
+
* The raw-column counterpart to {@link sightFrameAttachmentIdsFromMetadata},
|
|
302
|
+
* for consumers holding the JSON string a row was written with rather than a
|
|
303
|
+
* parsed bag: the memory rebuild's re-embed scan and the retrospective's
|
|
304
|
+
* accounting both ask this question of rows they never otherwise decode. It
|
|
305
|
+
* lives here, beside the key and the parser, so the two cannot drift.
|
|
306
|
+
*
|
|
307
|
+
* BOTH halves are required, and the tag alone is not enough. A genuine spoken
|
|
308
|
+
* turn carries the same tag whenever a parked frame rode it (see
|
|
309
|
+
* `calls/voice-session-bridge.ts`), which in camera mode is every turn the
|
|
310
|
+
* user speaks, so keying on the tag would read real speech as machine
|
|
311
|
+
* output. `scripted` is what separates them: the standalone persist sets it
|
|
312
|
+
* (`live-voice/live-voice-photo.ts`), while a spoken turn leaves it absent
|
|
313
|
+
* and the persist stamps the `false` default durably. The pair is the
|
|
314
|
+
* signature of a keep; `scripted` on its own belongs to every auto-sent row,
|
|
315
|
+
* onboarding prompts included, which are none of this predicate's business.
|
|
316
|
+
*
|
|
317
|
+
* Absent or unparseable metadata is not a keep. Both callers use this to hold
|
|
318
|
+
* keeps BACK from work, so a row whose marking cannot be read is treated as
|
|
319
|
+
* ordinary content and still gets indexed or reviewed, which is the direction
|
|
320
|
+
* that loses nothing.
|
|
321
|
+
*/
|
|
322
|
+
export function messageMetadataIsAmbientSightKeep(
|
|
323
|
+
metadata: string | null,
|
|
324
|
+
): boolean {
|
|
325
|
+
if (!metadata) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(metadata) as Record<string, unknown>;
|
|
330
|
+
return (
|
|
331
|
+
parsed.scripted === true &&
|
|
332
|
+
sightFrameAttachmentIdsFromMetadata(parsed).length > 0
|
|
333
|
+
);
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
230
339
|
/**
|
|
231
340
|
* True when the row that opened the turn was sent from a desktop app, on that
|
|
232
341
|
* row's own evidence.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { join, sep } from "node:path";
|
|
4
4
|
import { Database } from "bun:sqlite";
|
|
5
5
|
|
|
6
|
+
import { canonicalizePathThroughExistingParent } from "@vellumai/environments/test-path-guard";
|
|
6
7
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
7
8
|
|
|
8
9
|
import { getLogsDbPath } from "../util/logs-db-path.js";
|
|
@@ -15,25 +16,6 @@ import { wrapSqliteForSlowQueryLogging } from "./slow-query-log.js";
|
|
|
15
16
|
|
|
16
17
|
export type DrizzleDb = ReturnType<typeof drizzle<typeof schema>>;
|
|
17
18
|
|
|
18
|
-
function canonicalizePathThroughExistingParent(path: string): string {
|
|
19
|
-
const resolvedPath = resolve(path);
|
|
20
|
-
const pendingSegments: string[] = [];
|
|
21
|
-
let currentPath = resolvedPath;
|
|
22
|
-
|
|
23
|
-
while (true) {
|
|
24
|
-
try {
|
|
25
|
-
return resolve(realpathSync(currentPath), ...pendingSegments.reverse());
|
|
26
|
-
} catch {
|
|
27
|
-
const parentPath = dirname(currentPath);
|
|
28
|
-
if (parentPath === currentPath) {
|
|
29
|
-
return resolvedPath;
|
|
30
|
-
}
|
|
31
|
-
pendingSegments.push(basename(currentPath));
|
|
32
|
-
currentPath = parentPath;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
19
|
/**
|
|
38
20
|
* Guard against opening a real workspace database during tests. Shared by
|
|
39
21
|
* every connection opener (main, logs, memory) so a misconfigured test run
|
|
@@ -51,12 +51,12 @@ const SLACK_LEGACY_THREAD_EVIDENCE_BATCH_SIZE = 50;
|
|
|
51
51
|
const SLACK_LEGACY_THREAD_EVIDENCE_MAX_SCAN = 500;
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Rows examined when locating a
|
|
54
|
+
* Rows examined when locating a channel message the assistant itself posted.
|
|
55
55
|
* Bounded on purpose: the scan runs on the inbound path while the gateway
|
|
56
56
|
* waits for its ack, and reactions land on recent messages, so a cap costs
|
|
57
57
|
* almost no recall and keeps the cost flat as the database grows.
|
|
58
58
|
*/
|
|
59
|
-
const
|
|
59
|
+
const OUTBOUND_MESSAGE_ID_MAX_SCAN = 400;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Channels where an inbound thread id scopes the conversation: a Slack thread
|
|
@@ -295,8 +295,8 @@ export function findInboundEvent(
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
* The conversation holding the message with this provider id, found by
|
|
299
|
-
* the metadata the assistant's own posts carry.
|
|
298
|
+
* The conversation holding the message with this provider id, found by
|
|
299
|
+
* reading the metadata the assistant's own posts carry, on any channel.
|
|
300
300
|
*
|
|
301
301
|
* Reads through `readProviderMetadata`, so it matches any channel that
|
|
302
302
|
* describes its rows in the neutral shape as well as Slack's own envelope.
|
|
@@ -305,16 +305,18 @@ export function findInboundEvent(
|
|
|
305
305
|
* event. It cannot see what the assistant posted, because an outbound reply
|
|
306
306
|
* opens no inbound event, so a reaction on the assistant's own message needs
|
|
307
307
|
* this. The search is confined to conversations already bound to the same
|
|
308
|
-
*
|
|
309
|
-
* rows among them; beyond that it gives
|
|
310
|
-
* annotation, which is the same outcome as never
|
|
308
|
+
* channel address and to the most recent
|
|
309
|
+
* {@link OUTBOUND_MESSAGE_ID_MAX_SCAN} rows among them; beyond that it gives
|
|
310
|
+
* up and the caller drops the annotation, which is the same outcome as never
|
|
311
|
+
* finding it at all.
|
|
311
312
|
*/
|
|
312
|
-
export function
|
|
313
|
+
export function findConversationByProviderMessageId(
|
|
314
|
+
sourceChannel: string,
|
|
313
315
|
externalChatId: string,
|
|
314
|
-
|
|
316
|
+
providerMessageId: string,
|
|
315
317
|
): string | null {
|
|
316
318
|
const db = getDb();
|
|
317
|
-
const keyPrefix = `${CONVERSATION_KEY_SCOPE}
|
|
319
|
+
const keyPrefix = `${CONVERSATION_KEY_SCOPE}:${sourceChannel}:${externalChatId}`;
|
|
318
320
|
const rows = db
|
|
319
321
|
.select({
|
|
320
322
|
conversationId: messages.conversationId,
|
|
@@ -338,14 +340,15 @@ export function findSlackConversationByMessageTs(
|
|
|
338
340
|
),
|
|
339
341
|
)
|
|
340
342
|
.orderBy(desc(messages.createdAt))
|
|
341
|
-
.limit(
|
|
343
|
+
.limit(OUTBOUND_MESSAGE_ID_MAX_SCAN)
|
|
342
344
|
.all();
|
|
343
345
|
|
|
344
346
|
for (const row of rows) {
|
|
345
347
|
const meta = readProviderMetadata(row.metadata, { allowFlatLegacy: true });
|
|
346
348
|
if (
|
|
347
349
|
meta?.conversationExternalId === externalChatId &&
|
|
348
|
-
meta.messageId ===
|
|
350
|
+
(meta.messageId === providerMessageId ||
|
|
351
|
+
meta.additionalMessageIds?.includes(providerMessageId))
|
|
349
352
|
) {
|
|
350
353
|
return row.conversationId;
|
|
351
354
|
}
|
|
@@ -480,6 +483,33 @@ export function findMessageBySourceId(
|
|
|
480
483
|
return { messageId: row.messageId, conversationId: row.conversationId };
|
|
481
484
|
}
|
|
482
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Whether any inbound-event row exists for this source message, linked or
|
|
488
|
+
* not. Discriminates "never ingested" from "ingested, link still landing":
|
|
489
|
+
* only the second justifies waiting out the link race, so a delete or edit
|
|
490
|
+
* for a message the daemon never saw can return immediately instead of
|
|
491
|
+
* holding its conversation's serialized lane through the retry window.
|
|
492
|
+
*/
|
|
493
|
+
export function hasInboundEventForSource(
|
|
494
|
+
sourceChannel: string,
|
|
495
|
+
externalChatId: string,
|
|
496
|
+
sourceMessageId: string,
|
|
497
|
+
): boolean {
|
|
498
|
+
const db = getDb();
|
|
499
|
+
const row = db
|
|
500
|
+
.select({ id: channelInboundEvents.id })
|
|
501
|
+
.from(channelInboundEvents)
|
|
502
|
+
.where(
|
|
503
|
+
and(
|
|
504
|
+
eq(channelInboundEvents.sourceChannel, sourceChannel),
|
|
505
|
+
eq(channelInboundEvents.externalChatId, externalChatId),
|
|
506
|
+
eq(channelInboundEvents.sourceMessageId, sourceMessageId),
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
.get();
|
|
510
|
+
return row !== undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
483
513
|
/**
|
|
484
514
|
* Reference to the most recent inbound channel event for a conversation:
|
|
485
515
|
* the external chat plus the channel-native message identifiers needed to
|
|
@@ -18,8 +18,10 @@ import { getEmbedWorkerPidPath } from "../../../util/platform.js";
|
|
|
18
18
|
import {
|
|
19
19
|
classifyWorkerOwnership,
|
|
20
20
|
listWorkerProcesses,
|
|
21
|
-
|
|
22
|
-
} from "
|
|
21
|
+
} from "../../../util/worker-ownership.js";
|
|
22
|
+
import { ORPHAN_STOP_TIMEOUT_MS } from "../../../util/worker-process.js";
|
|
23
|
+
import { EMBEDDING_SHUTDOWN_BUDGET_MS } from "../embedding-backend.js";
|
|
24
|
+
import { LocalEmbeddingBackend } from "../embedding-local.js";
|
|
23
25
|
|
|
24
26
|
/** Reach past `private`, which is compile-time only, so tests drive real state. */
|
|
25
27
|
type Internals = any;
|
|
@@ -753,3 +755,14 @@ describe("model matching", () => {
|
|
|
753
755
|
).toEqual([proc.pid]);
|
|
754
756
|
});
|
|
755
757
|
});
|
|
758
|
+
|
|
759
|
+
// The daemon's orphan reclaim SIGTERMs a stale memory worker and waits
|
|
760
|
+
// ORPHAN_STOP_TIMEOUT_MS before SIGKILL. That worker's shutdown reaps the ONNX
|
|
761
|
+
// subprocess it owns, bounded at EMBEDDING_SHUTDOWN_BUDGET_MS + 1s. If the
|
|
762
|
+
// reclaim ceiling does not outlast the reap, reclaiming kills the worker
|
|
763
|
+
// mid-reap and strands the subprocess, which is the JARVIS-1125 failure.
|
|
764
|
+
test("orphan reclaim outlasts the embedding reap it must not truncate", () => {
|
|
765
|
+
expect(ORPHAN_STOP_TIMEOUT_MS).toBeGreaterThan(
|
|
766
|
+
EMBEDDING_SHUTDOWN_BUDGET_MS + 1_000,
|
|
767
|
+
);
|
|
768
|
+
});
|
|
@@ -13,13 +13,15 @@ import {
|
|
|
13
13
|
getEmbeddingModelsDir,
|
|
14
14
|
getEmbedWorkerPidPath,
|
|
15
15
|
} from "../../util/platform.js";
|
|
16
|
-
import {
|
|
17
|
-
listProcessTable,
|
|
18
|
-
readRawProcessCommand,
|
|
19
|
-
} from "../../util/process-table.js";
|
|
16
|
+
import { isProcessAlive } from "../../util/process-liveness.js";
|
|
20
17
|
import { PromiseGuard } from "../../util/promise-guard.js";
|
|
21
18
|
import { workerComputeEnv } from "../../util/worker-compute.js";
|
|
22
19
|
import { workerMemoryEnv } from "../../util/worker-memory.js";
|
|
20
|
+
import {
|
|
21
|
+
classifyWorkerOwnership,
|
|
22
|
+
listWorkerProcesses,
|
|
23
|
+
pid1OwnsWorkers,
|
|
24
|
+
} from "../../util/worker-ownership.js";
|
|
23
25
|
import { EmbeddingRuntimeManager } from "./embedding-runtime-manager.js";
|
|
24
26
|
import {
|
|
25
27
|
type EmbeddingBackend,
|
|
@@ -87,113 +89,6 @@ async function didSettle(
|
|
|
87
89
|
return result !== timeout;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
/** Whether a PID names a live process. */
|
|
91
|
-
function isProcessAlive(pid: number): boolean {
|
|
92
|
-
try {
|
|
93
|
-
// Signal 0 probes for liveness without delivering a signal.
|
|
94
|
-
process.kill(pid, 0);
|
|
95
|
-
return true;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface WorkerProcess {
|
|
102
|
-
pid: number;
|
|
103
|
-
ppid: number;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* What this process may do with an embed worker it found in the process table.
|
|
108
|
-
*
|
|
109
|
-
* - `reclaim`: parented to us, a worker we started and lost track of. Reaping
|
|
110
|
-
* it is what enforces one live worker per owning process.
|
|
111
|
-
* - `orphan`: reparented to init, or its owner is gone. Nobody else will ever
|
|
112
|
-
* clean it up.
|
|
113
|
-
* - `foreign`: owned by another live process. The memory-worker process runs
|
|
114
|
-
* its own backend against this same workspace and is entitled to its own
|
|
115
|
-
* worker; signalling it is what made the two replace each other in a loop.
|
|
116
|
-
*
|
|
117
|
-
* A parent of PID 1 is ambiguous, and resolving it wrongly in either direction
|
|
118
|
-
* costs something real. `docker-entrypoint.sh` execs the daemon, so PID 1 can
|
|
119
|
-
* BE the daemon and its child a healthy sibling's worker. Under `docker run
|
|
120
|
-
* --init`, and on any host, PID 1 is an init process instead, so the same
|
|
121
|
-
* parentage means the owner died and the worker needs reclaiming. Callers
|
|
122
|
-
* therefore pass what PID 1 actually is rather than inferring it from whether
|
|
123
|
-
* the deployment is containerized, which is true in both container shapes.
|
|
124
|
-
*/
|
|
125
|
-
export type WorkerOwnership = "reclaim" | "orphan" | "foreign";
|
|
126
|
-
|
|
127
|
-
export function classifyWorkerOwnership(
|
|
128
|
-
worker: WorkerProcess,
|
|
129
|
-
selfPid: number,
|
|
130
|
-
isOwnerAlive: (pid: number) => boolean,
|
|
131
|
-
pid1OwnsWorkers: boolean,
|
|
132
|
-
): WorkerOwnership {
|
|
133
|
-
if (worker.ppid === selfPid) {
|
|
134
|
-
return "reclaim";
|
|
135
|
-
}
|
|
136
|
-
if (worker.ppid <= 1) {
|
|
137
|
-
return pid1OwnsWorkers ? "foreign" : "orphan";
|
|
138
|
-
}
|
|
139
|
-
if (!isOwnerAlive(worker.ppid)) {
|
|
140
|
-
return "orphan";
|
|
141
|
-
}
|
|
142
|
-
return "foreign";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Entrypoint the daemon is exec'd with, including inside a container. */
|
|
146
|
-
const DAEMON_ENTRYPOINT_MARKER = "daemon/main";
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Whether PID 1 is an assistant daemon rather than an init process.
|
|
150
|
-
*
|
|
151
|
-
* True only where the daemon was exec'd as PID 1 (`docker-entrypoint.sh`),
|
|
152
|
-
* which is what makes a worker parented to 1 a live sibling's rather than an
|
|
153
|
-
* orphan. Under `docker run --init` PID 1 is docker-init and under launchd or
|
|
154
|
-
* systemd it is the system init, so both answer false and PID-1 orphans stay
|
|
155
|
-
* reclaimable. Unreadable means false, which keeps the reclaiming behaviour.
|
|
156
|
-
*/
|
|
157
|
-
function pid1OwnsEmbedWorkers(): boolean {
|
|
158
|
-
if (process.pid === 1) {
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
return readRawProcessCommand(1)?.includes(DAEMON_ENTRYPOINT_MARKER) ?? false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Live embed-worker processes for this workspace, as `(pid, ppid)` pairs.
|
|
166
|
-
*
|
|
167
|
-
* Ownership of an embed worker is recorded by the OS process tree, not by the
|
|
168
|
-
* PID file: a worker's parent IS its owner. The process table is therefore the
|
|
169
|
-
* authoritative answer to "whose worker is this", and unlike the PID file it
|
|
170
|
-
* survives a crash and cannot be overwritten by a second owner. That matters
|
|
171
|
-
* because the daemon and the memory-worker process legitimately run one worker
|
|
172
|
-
* each against the same workspace.
|
|
173
|
-
*
|
|
174
|
-
* Matches on the absolute worker script path, which lives under THIS
|
|
175
|
-
* workspace's embedding-models directory and is therefore unique per assistant
|
|
176
|
-
* instance, so a sibling instance's workers never match. Raw command lines are
|
|
177
|
-
* read for matching only, never stored or logged: process arguments can carry
|
|
178
|
-
* secrets (see the redaction note in `util/process-tree.ts`).
|
|
179
|
-
*/
|
|
180
|
-
export function listWorkerProcesses(
|
|
181
|
-
workerPath: string,
|
|
182
|
-
model?: string,
|
|
183
|
-
): WorkerProcess[] {
|
|
184
|
-
try {
|
|
185
|
-
return listProcessTable()
|
|
186
|
-
.filter(
|
|
187
|
-
(row) =>
|
|
188
|
-
row.command.includes(workerPath) &&
|
|
189
|
-
(!model || row.command.split(/\s+/).includes(model)),
|
|
190
|
-
)
|
|
191
|
-
.map(({ pid, ppid }) => ({ pid, ppid }));
|
|
192
|
-
} catch {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
92
|
/**
|
|
198
93
|
* Local embedding backend using @huggingface/transformers (ONNX Runtime).
|
|
199
94
|
* Runs BAAI/bge-small-en-v1.5 locally — no API calls, no network required.
|
|
@@ -874,7 +769,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
874
769
|
worker,
|
|
875
770
|
process.pid,
|
|
876
771
|
isProcessAlive,
|
|
877
|
-
|
|
772
|
+
pid1OwnsWorkers(),
|
|
878
773
|
);
|
|
879
774
|
if (ownership === "foreign") {
|
|
880
775
|
continue;
|
|
@@ -51,7 +51,9 @@ export function resolveQdrantReleaseAsset(
|
|
|
51
51
|
target = "x86_64-unknown-linux-musl";
|
|
52
52
|
} else if (os === "linux" && cpu === "arm64") {
|
|
53
53
|
target = "aarch64-unknown-linux-musl";
|
|
54
|
-
} else if (os === "win32" && cpu === "x64") {
|
|
54
|
+
} else if (os === "win32" && (cpu === "x64" || cpu === "arm64")) {
|
|
55
|
+
// Qdrant ships no arm64 Windows build; Windows 11 on ARM runs the x64
|
|
56
|
+
// binary via transparent emulation.
|
|
55
57
|
return {
|
|
56
58
|
binaryName: "qdrant.exe",
|
|
57
59
|
filename: "qdrant-x86_64-pc-windows-msvc.zip",
|
|
@@ -286,6 +288,7 @@ export class QdrantManager {
|
|
|
286
288
|
cmd: ["tar", "xzf", tmpTar, "-C", binDir, release.binaryName],
|
|
287
289
|
stdout: "ignore",
|
|
288
290
|
stderr: "pipe",
|
|
291
|
+
windowsHide: true,
|
|
289
292
|
});
|
|
290
293
|
await proc.exited;
|
|
291
294
|
if (proc.exitCode !== 0) {
|
|
@@ -346,7 +346,7 @@ export function recordRequestLog(
|
|
|
346
346
|
// plumb call sites through one site at a time.
|
|
347
347
|
callSite: callSite ?? null,
|
|
348
348
|
// JSON first-token latency waterfall, supplied by `handleUsage` for
|
|
349
|
-
//
|
|
349
|
+
// instrumented calls. NULL for failed/non-instrumented call paths.
|
|
350
350
|
latencyBreakdown: latencyBreakdown ?? null,
|
|
351
351
|
});
|
|
352
352
|
return id;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const TABLE = "acp_session_history";
|
|
5
|
+
const COLUMN = "auth_error_code";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add a nullable `auth_error_code TEXT` column to `acp_session_history`.
|
|
9
|
+
*
|
|
10
|
+
* Names the credential failure that ended a run, so a client that reopens the
|
|
11
|
+
* conversation can re-raise the inline "Connect Claude Code" card for it. The
|
|
12
|
+
* row is the right home for that: it already carries the parent conversation
|
|
13
|
+
* and the spawning tool call the card anchors to, nothing rewrites it the way
|
|
14
|
+
* a turn's finalize rewrites message content, and it outlives the daemon
|
|
15
|
+
* process, so retirement does not depend on anything held in memory.
|
|
16
|
+
*
|
|
17
|
+
* `NULL` for rows persisted before this migration ran, and for every run that
|
|
18
|
+
* ended for any other reason.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent: the PRAGMA guard makes re-running a no-op once the column
|
|
21
|
+
* exists.
|
|
22
|
+
*/
|
|
23
|
+
export function migrateAcpSessionHistoryAuthErrorCode(
|
|
24
|
+
database: DrizzleDb,
|
|
25
|
+
): void {
|
|
26
|
+
const raw = getSqliteFrom(database);
|
|
27
|
+
|
|
28
|
+
const columns = raw.query(`PRAGMA table_info(${TABLE})`).all() as Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
}>;
|
|
31
|
+
const columnNames = new Set(columns.map((c) => c.name));
|
|
32
|
+
|
|
33
|
+
if (!columnNames.has(COLUMN)) {
|
|
34
|
+
raw.exec(`ALTER TABLE ${TABLE} ADD COLUMN ${COLUMN} TEXT`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const TABLE = "acp_session_history";
|
|
5
|
+
const COLUMN = "auth_error_credential";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add a nullable `auth_error_credential TEXT` column to `acp_session_history`.
|
|
9
|
+
*
|
|
10
|
+
* Holds a digest of the Claude token the run was using when Claude refused it,
|
|
11
|
+
* beside the `auth_error_code` that names the refusal. Together they let a
|
|
12
|
+
* marker answer for itself whether it is still worth showing: a client asks
|
|
13
|
+
* whether the credential named here is still the one a spawn would resolve,
|
|
14
|
+
* rather than relying on a sweep having run at the right moment to clear it.
|
|
15
|
+
*
|
|
16
|
+
* A digest, never the token. It only ever has to be compared for equality, and
|
|
17
|
+
* this outlives the run that produced it.
|
|
18
|
+
*
|
|
19
|
+
* `NULL` for rows written before this migration ran, and for every run that
|
|
20
|
+
* ended for any other reason. A marker with no credential named is served
|
|
21
|
+
* rather than hidden, since an unknown credential is no evidence the failure
|
|
22
|
+
* was repaired.
|
|
23
|
+
*
|
|
24
|
+
* Idempotent: the PRAGMA guard makes re-running a no-op once the column
|
|
25
|
+
* exists.
|
|
26
|
+
*/
|
|
27
|
+
export function migrateAcpSessionHistoryAuthErrorCredential(
|
|
28
|
+
database: DrizzleDb,
|
|
29
|
+
): void {
|
|
30
|
+
const raw = getSqliteFrom(database);
|
|
31
|
+
|
|
32
|
+
const columns = raw.query(`PRAGMA table_info(${TABLE})`).all() as Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
}>;
|
|
35
|
+
const columnNames = new Set(columns.map((c) => c.name));
|
|
36
|
+
|
|
37
|
+
if (!columnNames.has(COLUMN)) {
|
|
38
|
+
raw.exec(`ALTER TABLE ${TABLE} ADD COLUMN ${COLUMN} TEXT`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const TABLE = "acp_refused_credentials";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create `acp_refused_credentials`: which Claude tokens Claude has refused.
|
|
8
|
+
*
|
|
9
|
+
* Its own table rather than a column on `acp_session_history`, because the two
|
|
10
|
+
* answer different questions with different lifetimes. A marker says "show a
|
|
11
|
+
* card for this run" and is fair game for the user to delete along with the
|
|
12
|
+
* run. This says "do not resolve this token again", and clearing session
|
|
13
|
+
* history must not change which credential a spawn selects: a configured
|
|
14
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` lives in config, so forgetting that Claude refused
|
|
15
|
+
* it lets it win over the vault replacement again and reopen the Connect loop.
|
|
16
|
+
*
|
|
17
|
+
* Digests, never tokens. Equality is the only operation, and these records
|
|
18
|
+
* deliberately outlive the runs that produced them.
|
|
19
|
+
*
|
|
20
|
+
* Retained rather than consumed: a revoked value the user never removes from
|
|
21
|
+
* config is resolved by every later spawn too, so a one-shot only spares the
|
|
22
|
+
* first. Keeping them is safe because a value the user actually fixes hashes
|
|
23
|
+
* differently and was never recorded.
|
|
24
|
+
*
|
|
25
|
+
* Idempotent via IF NOT EXISTS.
|
|
26
|
+
*/
|
|
27
|
+
export function migrateCreateAcpRefusedCredentials(database: DrizzleDb): void {
|
|
28
|
+
getSqliteFrom(database).exec(
|
|
29
|
+
`CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
30
|
+
digest TEXT PRIMARY KEY,
|
|
31
|
+
refused_at INTEGER NOT NULL
|
|
32
|
+
)`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const INDEX = "idx_acp_session_history_auth_marker";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Index the credential-failure markers a conversation carries.
|
|
8
|
+
*
|
|
9
|
+
* Every snapshot refetch asks the same question: the newest marked runs in one
|
|
10
|
+
* conversation. The existing indexes cover the parent and the timestamp
|
|
11
|
+
* separately, so that question meant gathering a conversation's rows through
|
|
12
|
+
* one and then sorting them, work that grows with how many runs it has had
|
|
13
|
+
* rather than with the handful the answer needs.
|
|
14
|
+
*
|
|
15
|
+
* Partial, on the marker predicate, so the index holds only rows that carry a
|
|
16
|
+
* failure. Conversations that have never had one contribute nothing to it, and
|
|
17
|
+
* the ordered columns let the scan stop as soon as it has enough.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent via IF NOT EXISTS.
|
|
20
|
+
*/
|
|
21
|
+
export function migrateAcpAuthMarkerIndex(database: DrizzleDb): void {
|
|
22
|
+
getSqliteFrom(database).exec(
|
|
23
|
+
`CREATE INDEX IF NOT EXISTS ${INDEX}
|
|
24
|
+
ON acp_session_history (parent_conversation_id, started_at DESC)
|
|
25
|
+
WHERE auth_error_code IS NOT NULL`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 370 adds the nullable `auth_error_code` column that carries a
|
|
3
|
+
* run's credential failure across a reload, and is idempotent so a repair flow
|
|
4
|
+
* can re-run it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
11
|
+
|
|
12
|
+
import { getSqliteFrom } from "../../db-connection.js";
|
|
13
|
+
import * as schema from "../../schema.js";
|
|
14
|
+
import { migrateAcpSessionHistoryAuthErrorCode } from "../370-acp-session-history-auth-error-code.js";
|
|
15
|
+
|
|
16
|
+
function createTestDb() {
|
|
17
|
+
const sqlite = new Database(":memory:");
|
|
18
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
19
|
+
sqlite.exec(
|
|
20
|
+
"CREATE TABLE acp_session_history (id TEXT PRIMARY KEY, status TEXT)",
|
|
21
|
+
);
|
|
22
|
+
return drizzle(sqlite, { schema });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function columnNames(db: ReturnType<typeof createTestDb>): string[] {
|
|
26
|
+
const rows = getSqliteFrom(db)
|
|
27
|
+
.prepare("PRAGMA table_info(acp_session_history)")
|
|
28
|
+
.all() as { name: string }[];
|
|
29
|
+
return rows.map((row) => row.name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("migrateAcpSessionHistoryAuthErrorCode", () => {
|
|
33
|
+
test("adds the column when it is missing", () => {
|
|
34
|
+
const db = createTestDb();
|
|
35
|
+
|
|
36
|
+
migrateAcpSessionHistoryAuthErrorCode(db);
|
|
37
|
+
|
|
38
|
+
expect(columnNames(db)).toContain("auth_error_code");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("re-running is a no-op", () => {
|
|
42
|
+
const db = createTestDb();
|
|
43
|
+
|
|
44
|
+
migrateAcpSessionHistoryAuthErrorCode(db);
|
|
45
|
+
migrateAcpSessionHistoryAuthErrorCode(db);
|
|
46
|
+
|
|
47
|
+
expect(columnNames(db)).toContain("auth_error_code");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("leaves existing rows null rather than inventing a failure", () => {
|
|
51
|
+
const db = createTestDb();
|
|
52
|
+
getSqliteFrom(db).exec(
|
|
53
|
+
"INSERT INTO acp_session_history (id, status) VALUES ('run-1', 'completed')",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
migrateAcpSessionHistoryAuthErrorCode(db);
|
|
57
|
+
|
|
58
|
+
const row = getSqliteFrom(db)
|
|
59
|
+
.prepare(
|
|
60
|
+
"SELECT auth_error_code FROM acp_session_history WHERE id = 'run-1'",
|
|
61
|
+
)
|
|
62
|
+
.get() as { auth_error_code: string | null };
|
|
63
|
+
expect(row.auth_error_code).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|