@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
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
ServicesSchema,
|
|
20
20
|
} from "../../config/schemas/services.js";
|
|
21
21
|
import type { OAuthConnectionRequest } from "../../oauth/connection.js";
|
|
22
|
-
import { jsonSafeOAuthBody } from "../../oauth/connection.js";
|
|
22
|
+
import { isBinaryOAuthBody, jsonSafeOAuthBody } from "../../oauth/connection.js";
|
|
23
23
|
import {
|
|
24
24
|
resolveOAuthConnection,
|
|
25
25
|
type ResolveOAuthConnectionOptions,
|
|
@@ -42,6 +42,7 @@ import { matchHostPattern } from "../../tools/credentials/host-pattern-match.js"
|
|
|
42
42
|
import { getLogger } from "../../util/logger.js";
|
|
43
43
|
import {
|
|
44
44
|
findContentTypeHeader,
|
|
45
|
+
parseRequestBodyBytes,
|
|
45
46
|
parseRequestBodyData,
|
|
46
47
|
} from "../../util/oauth-request-body.js";
|
|
47
48
|
import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
|
|
@@ -729,7 +730,8 @@ async function handleToken({ body = {} }: RouteHandlerArgs) {
|
|
|
729
730
|
/**
|
|
730
731
|
* Resolve a raw `data` string into a request body. A non-JSON Content-Type
|
|
731
732
|
* keeps the payload as the caller's exact string so multipart and other
|
|
732
|
-
* byte-sensitive payloads survive
|
|
733
|
+
* byte-sensitive payloads survive. Files are read as raw bytes: valid UTF-8
|
|
734
|
+
* stays text, and anything else stays a Buffer.
|
|
733
735
|
*/
|
|
734
736
|
function readBodyData(data: string, contentType: string | undefined): unknown {
|
|
735
737
|
if (data === "@-") {
|
|
@@ -744,7 +746,7 @@ function readBodyData(data: string, contentType: string | undefined): unknown {
|
|
|
744
746
|
|
|
745
747
|
if (data.startsWith("@")) {
|
|
746
748
|
const filePath = data.slice(1);
|
|
747
|
-
return
|
|
749
|
+
return parseRequestBodyBytes(readFileSync(filePath), contentType);
|
|
748
750
|
}
|
|
749
751
|
|
|
750
752
|
return parseRequestBodyData(data, contentType);
|
|
@@ -760,6 +762,8 @@ export async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
|
760
762
|
parsed_data?: unknown;
|
|
761
763
|
/** Raw data string (for direct API callers, not the CLI). */
|
|
762
764
|
data?: string;
|
|
765
|
+
/** When set to base64, `parsed_data` / `data` is a base64 string of raw bytes. */
|
|
766
|
+
body_encoding?: "base64";
|
|
763
767
|
force_get?: boolean;
|
|
764
768
|
head?: boolean;
|
|
765
769
|
account?: string;
|
|
@@ -864,7 +868,15 @@ export async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
|
864
868
|
: undefined;
|
|
865
869
|
|
|
866
870
|
if (resolvedData !== undefined) {
|
|
867
|
-
|
|
871
|
+
let rawBody = resolvedData;
|
|
872
|
+
if (b.body_encoding === "base64") {
|
|
873
|
+
if (typeof rawBody !== "string") {
|
|
874
|
+
throw new BadRequestError(
|
|
875
|
+
"body_encoding=base64 requires a string body",
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
rawBody = Buffer.from(rawBody, "base64");
|
|
879
|
+
}
|
|
868
880
|
|
|
869
881
|
if (b.force_get) {
|
|
870
882
|
if (typeof rawBody === "string") {
|
|
@@ -882,7 +894,8 @@ export async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
|
882
894
|
} else if (
|
|
883
895
|
rawBody !== null &&
|
|
884
896
|
typeof rawBody === "object" &&
|
|
885
|
-
!Array.isArray(rawBody)
|
|
897
|
+
!Array.isArray(rawBody) &&
|
|
898
|
+
!isBinaryOAuthBody(rawBody)
|
|
886
899
|
) {
|
|
887
900
|
for (const [key, value] of Object.entries(
|
|
888
901
|
rawBody as Record<string, unknown>,
|
|
@@ -50,7 +50,7 @@ const statusResponseSchema = z.object({
|
|
|
50
50
|
async function startRouteHostWorker() {
|
|
51
51
|
let result: { pid: number; alreadyRunning: boolean };
|
|
52
52
|
try {
|
|
53
|
-
result = await spawnRouteHostWorkerProcess(
|
|
53
|
+
result = await spawnRouteHostWorkerProcess();
|
|
54
54
|
} catch (err) {
|
|
55
55
|
const message =
|
|
56
56
|
err instanceof RouteHostSpawnError || err instanceof Error
|
|
@@ -50,7 +50,7 @@ const statusResponseSchema = z.object({
|
|
|
50
50
|
async function startScheduleWorker() {
|
|
51
51
|
let result: { pid: number; alreadyRunning: boolean };
|
|
52
52
|
try {
|
|
53
|
-
result = await spawnScheduleWorkerProcess(
|
|
53
|
+
result = await spawnScheduleWorkerProcess();
|
|
54
54
|
} catch (err) {
|
|
55
55
|
const message =
|
|
56
56
|
err instanceof ScheduleWorkerSpawnError || err instanceof Error
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - Notification routing goes through emitNotificationSignal().
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { isGuardianRequestExpired } from "@vellumai/gateway-client";
|
|
15
|
+
|
|
14
16
|
import {
|
|
15
17
|
createGuardianRequest,
|
|
16
18
|
listGuardianRequestsOrEmpty,
|
|
@@ -124,8 +126,10 @@ export async function createOrReuseToolGrantRequest(
|
|
|
124
126
|
kind: "tool_grant_request",
|
|
125
127
|
toolName,
|
|
126
128
|
});
|
|
129
|
+
// Past-deadline rows never dedupe: see the access-request helper's note.
|
|
127
130
|
const dedupeMatch = existing.find(
|
|
128
131
|
(r) =>
|
|
132
|
+
!isGuardianRequestExpired(r) &&
|
|
129
133
|
r.inputDigest === inputDigest &&
|
|
130
134
|
r.guardianExternalUserId === binding.guardianExternalUserId,
|
|
131
135
|
);
|
|
@@ -360,7 +360,7 @@ export function startScheduler(): SchedulerHandle {
|
|
|
360
360
|
scheduleWorkerSupervisor = createWorkerSupervisor({
|
|
361
361
|
label: "Schedule worker",
|
|
362
362
|
probe: probeScheduleWorker,
|
|
363
|
-
respawn: () => spawnScheduleWorkerProcess(
|
|
363
|
+
respawn: () => spawnScheduleWorkerProcess(),
|
|
364
364
|
isSuppressed: isScheduleWorkerAdministrativelyStopped,
|
|
365
365
|
killChild: (pid) => {
|
|
366
366
|
try {
|
|
@@ -119,13 +119,13 @@ export function stopScheduleWorkerProcess(): WorkerProcessStatus {
|
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
121
|
* Daemon-lifecycle entry point: spawn the schedule worker as a child of the
|
|
122
|
-
* daemon (
|
|
122
|
+
* daemon (so it appears in the daemon's process tree and is torn down
|
|
123
123
|
* on shutdown). Fire-and-forget — a worker failure must never block boot. A
|
|
124
124
|
* worker that comes up late is still the desired sole schedule runner
|
|
125
125
|
* (`terminateOnTimeout` is deliberately not set).
|
|
126
126
|
*/
|
|
127
127
|
export function startScheduleWorker(): void {
|
|
128
|
-
void spawnScheduleWorkerProcess(
|
|
128
|
+
void spawnScheduleWorkerProcess()
|
|
129
129
|
.then((r) =>
|
|
130
130
|
log.info(
|
|
131
131
|
{ pid: r.pid, alreadyRunning: r.alreadyRunning },
|
package/src/security/oauth2.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
20
20
|
import { createServer, type Server } from "node:http";
|
|
21
21
|
|
|
22
22
|
import { getIsPlatform } from "../config/env-registry.js";
|
|
23
|
+
import { parseScopeList } from "../oauth/scope-utils.js";
|
|
23
24
|
import { getLogger } from "../util/logger.js";
|
|
24
25
|
import { renderOAuthCompletionPage as renderLoopbackPage } from "./oauth-completion-page.js";
|
|
25
26
|
|
|
@@ -249,19 +250,9 @@ export async function exchangeCodeForTokens(
|
|
|
249
250
|
(tokenData.token_type as string | undefined),
|
|
250
251
|
};
|
|
251
252
|
|
|
252
|
-
// Defensive split: providers (e.g. GitHub, Slack) may return comma-separated
|
|
253
|
-
// scopes in token responses regardless of the scope_separator used to join
|
|
254
|
-
// outbound authorize URLs, so we tolerate both spaces and commas here. When
|
|
255
|
-
// a provider explicitly configures a non-default separator (e.g. Linear uses
|
|
256
|
-
// ","), we honor that to keep symmetric round-tripping of configured scopes.
|
|
257
|
-
const splitPattern =
|
|
258
|
-
config.scopeSeparator === " " ? /[ ,]/ : config.scopeSeparator;
|
|
259
253
|
const grantedScopes =
|
|
260
254
|
typeof tokens.scope === "string"
|
|
261
|
-
? tokens.scope
|
|
262
|
-
.split(splitPattern)
|
|
263
|
-
.map((s) => s.trim())
|
|
264
|
-
.filter(Boolean)
|
|
255
|
+
? parseScopeList(tokens.scope, config.scopeSeparator)
|
|
265
256
|
: [...config.scopes];
|
|
266
257
|
|
|
267
258
|
return { tokens, grantedScopes, rawTokenResponse: tokenData };
|
|
@@ -60,6 +60,7 @@ export type {
|
|
|
60
60
|
CredentialListResult,
|
|
61
61
|
DeleteResult,
|
|
62
62
|
} from "./credential-backend.js";
|
|
63
|
+
import { ACP_OAUTH_TOKEN_FIELD, ACP_SERVICE } from "../acp/acp-credentials.js";
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Re-export shared-package secure-key abstractions so downstream consumers
|
|
@@ -673,8 +674,40 @@ export async function getSecureKeyAsync(
|
|
|
673
674
|
}
|
|
674
675
|
|
|
675
676
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
677
|
+
* Post-write side effects keyed on which credential was actually stored.
|
|
678
|
+
*
|
|
679
|
+
* Every path that lands a credential runs through this, single writes and bulk
|
|
680
|
+
* restores alike, so a behaviour that has to follow a credential does not
|
|
681
|
+
* depend on a list of call sites that drifts as write paths are added.
|
|
682
|
+
*
|
|
683
|
+
* Deliberately not where a Connect card is retired. A card is retired by the
|
|
684
|
+
* marker it came from no longer matching the credential a spawn would resolve,
|
|
685
|
+
* which is a comparison made when the marker is read. Nothing here has to fire
|
|
686
|
+
* at the right moment for that to happen, so this is a notification rather
|
|
687
|
+
* than a mechanism, and a failure costs freshness rather than correctness.
|
|
688
|
+
*
|
|
689
|
+
* Imported on demand and only on a match, so the ACP module graph stays out of
|
|
690
|
+
* this module's load path.
|
|
691
|
+
*/
|
|
692
|
+
async function onCredentialsWritten(accounts: string[]): Promise<void> {
|
|
693
|
+
if (!accounts.includes(credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD))) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
// The Connect flow repairs the spawn policy *after* this write, so a token
|
|
698
|
+
// whose read is still denied here is retried by `storeAcpClaudeToken` once
|
|
699
|
+
// that repair lands. This pass covers every other writer.
|
|
700
|
+
const { notifyAcpConnectRetired } =
|
|
701
|
+
await import("../acp/acp-claude-oauth.js");
|
|
702
|
+
await notifyAcpConnectRetired();
|
|
703
|
+
} catch (err) {
|
|
704
|
+
log.warn({ err }, "ACP Connect card notification failed after a write");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Store a secret in secure storage. Writes to exactly one backend, with no
|
|
710
|
+
* dual-writing. Forces a CES reconnection when the backend is dead, because
|
|
678
711
|
* callers (e.g. the post-login platform credential push) do not retry a
|
|
679
712
|
* failed write.
|
|
680
713
|
*/
|
|
@@ -682,10 +715,10 @@ export async function setSecureKeyAsync(
|
|
|
682
715
|
account: string,
|
|
683
716
|
value: string,
|
|
684
717
|
): Promise<boolean> {
|
|
685
|
-
|
|
718
|
+
const ok = await withCredentialTimeout(async () => {
|
|
686
719
|
const backend = await resolveBackendAsync({ forceReconnect: true });
|
|
687
|
-
const
|
|
688
|
-
if (!
|
|
720
|
+
const stored = await backend.set(account, value);
|
|
721
|
+
if (!stored) {
|
|
689
722
|
log.warn(
|
|
690
723
|
{ account, backend: backend.name },
|
|
691
724
|
"Credential backend set failed",
|
|
@@ -693,9 +726,19 @@ export async function setSecureKeyAsync(
|
|
|
693
726
|
} else {
|
|
694
727
|
log.info({ account, backend: backend.name }, "Credential stored");
|
|
695
728
|
}
|
|
696
|
-
updateCesHttpReachability(backend, !
|
|
697
|
-
return
|
|
729
|
+
updateCesHttpReachability(backend, !stored);
|
|
730
|
+
return stored;
|
|
698
731
|
}, false);
|
|
732
|
+
// Detached, because it neither decides the result nor gates it. Under the
|
|
733
|
+
// deadline its cost could turn a stored credential into a reported failure;
|
|
734
|
+
// awaited after it, the cost merely held the caller at "signing in" while a
|
|
735
|
+
// scan and a broadcast finished. Nothing downstream depends on it having
|
|
736
|
+
// run: a card is retired by the marker comparison at read time, and the
|
|
737
|
+
// credential prompt re-checks the registry entry before honouring it.
|
|
738
|
+
if (ok) {
|
|
739
|
+
void onCredentialsWritten([account]);
|
|
740
|
+
}
|
|
741
|
+
return ok;
|
|
699
742
|
}
|
|
700
743
|
|
|
701
744
|
/**
|
|
@@ -726,7 +769,7 @@ export async function deleteSecureKeyAsync(
|
|
|
726
769
|
export async function bulkSetSecureKeysAsync(
|
|
727
770
|
credentials: Array<{ account: string; value: string }>,
|
|
728
771
|
): Promise<Array<{ account: string; ok: boolean }>> {
|
|
729
|
-
|
|
772
|
+
const results = await withCredentialTimeout(
|
|
730
773
|
async () => {
|
|
731
774
|
const backend = await resolveBackendAsync({ forceReconnect: true });
|
|
732
775
|
let results: Array<{ account: string; ok: boolean }>;
|
|
@@ -760,6 +803,10 @@ export async function bulkSetSecureKeysAsync(
|
|
|
760
803
|
},
|
|
761
804
|
credentials.map((c) => ({ account: c.account, ok: false })),
|
|
762
805
|
);
|
|
806
|
+
// Same as the single-write path: detached, so a bundle's notifications
|
|
807
|
+
// neither decide nor delay its result.
|
|
808
|
+
void onCredentialsWritten(results.filter((r) => r.ok).map((r) => r.account));
|
|
809
|
+
return results;
|
|
763
810
|
}
|
|
764
811
|
|
|
765
812
|
// ---------------------------------------------------------------------------
|
|
@@ -16,8 +16,9 @@ import { gunzipSync } from "node:zlib";
|
|
|
16
16
|
|
|
17
17
|
import { getPlatformBaseUrl } from "../config/env.js";
|
|
18
18
|
import { loadSkillCatalog } from "../config/skills.js";
|
|
19
|
+
import { isBunVirtualPath } from "../util/bundled-asset.js";
|
|
19
20
|
import { getLogger } from "../util/logger.js";
|
|
20
|
-
import { getWorkspaceSkillsDir } from "../util/platform.js";
|
|
21
|
+
import { addToPathEnv, getWorkspaceSkillsDir } from "../util/platform.js";
|
|
21
22
|
import { computeSkillHash, writeInstallMeta } from "./install-meta.js";
|
|
22
23
|
import {
|
|
23
24
|
isSkillCompatibleWithPlatform,
|
|
@@ -67,7 +68,7 @@ export interface CatalogSkill {
|
|
|
67
68
|
export function getRepoSkillsDir(): string | undefined {
|
|
68
69
|
const importDir = import.meta.dir;
|
|
69
70
|
|
|
70
|
-
if (importDir
|
|
71
|
+
if (isBunVirtualPath(importDir)) {
|
|
71
72
|
const execDir = dirname(process.execPath);
|
|
72
73
|
// macOS .app bundle: binary in Contents/MacOS/, resources in Contents/Resources/
|
|
73
74
|
const resourcesPath = join(
|
|
@@ -415,12 +416,14 @@ export async function installSkillDependenciesIfPresent(
|
|
|
415
416
|
if (!existsSync(join(skillDir, "package.json"))) {
|
|
416
417
|
return;
|
|
417
418
|
}
|
|
418
|
-
const
|
|
419
|
+
const env = { ...process.env };
|
|
420
|
+
addToPathEnv(env, [join(homedir(), ".bun", "bin")]);
|
|
419
421
|
await new Promise<void>((resolve, reject) => {
|
|
420
422
|
const child = spawn("bun", ["install"], {
|
|
421
423
|
cwd: skillDir,
|
|
422
424
|
stdio: "inherit",
|
|
423
|
-
env
|
|
425
|
+
env,
|
|
426
|
+
windowsHide: true,
|
|
424
427
|
});
|
|
425
428
|
child.on("error", reject);
|
|
426
429
|
child.on("close", (code) => {
|
package/src/skills/clawhub.ts
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
NOISE_FLOOR_BLOCK_COUNT,
|
|
5
|
+
NOISE_FLOOR_BLOCK_MS,
|
|
6
|
+
RoomNoiseFloor,
|
|
7
|
+
} from "../room-noise-floor.js";
|
|
8
|
+
|
|
9
|
+
/** Audio needed before the estimator will report anything at all. */
|
|
10
|
+
const FULL_WINDOW_MS = NOISE_FLOOR_BLOCK_MS * NOISE_FLOOR_BLOCK_COUNT;
|
|
11
|
+
|
|
12
|
+
/** Feed `ms` of audio at a steady amplitude, in 50ms chunks like the client. */
|
|
13
|
+
function feed(floor: RoomNoiseFloor, amplitude: number, ms: number): void {
|
|
14
|
+
for (let elapsed = 0; elapsed < ms; elapsed += 50) {
|
|
15
|
+
floor.observe(amplitude, 50);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("RoomNoiseFloor", () => {
|
|
20
|
+
test("has no opinion until the window is full", () => {
|
|
21
|
+
const floor = new RoomNoiseFloor();
|
|
22
|
+
feed(floor, 300, FULL_WINDOW_MS - NOISE_FLOOR_BLOCK_MS);
|
|
23
|
+
|
|
24
|
+
// A partial window would report the loudest thing it has heard as the room.
|
|
25
|
+
expect(floor.floor).toBeNull();
|
|
26
|
+
|
|
27
|
+
feed(floor, 300, NOISE_FLOOR_BLOCK_MS);
|
|
28
|
+
expect(floor.floor).toBeCloseTo(300, 5);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("ignores intermittent speech and keeps the quiet second", () => {
|
|
32
|
+
const floor = new RoomNoiseFloor();
|
|
33
|
+
// Someone talking over a 250-level room, with one pause in the window.
|
|
34
|
+
feed(floor, 2_400, FULL_WINDOW_MS - NOISE_FLOOR_BLOCK_MS);
|
|
35
|
+
feed(floor, 250, NOISE_FLOOR_BLOCK_MS);
|
|
36
|
+
|
|
37
|
+
// The minimum picks the second nobody spoke in, not the average of the ten.
|
|
38
|
+
expect(floor.floor).toBeCloseTo(250, 5);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a block averages across syllable gaps rather than latching onto one", () => {
|
|
42
|
+
const floor = new RoomNoiseFloor();
|
|
43
|
+
// Speech with brief pauses inside it: alternating loud and room-level
|
|
44
|
+
// chunks throughout. A per-chunk minimum would call this room 250.
|
|
45
|
+
for (let elapsed = 0; elapsed < FULL_WINDOW_MS; elapsed += 100) {
|
|
46
|
+
floor.observe(2_400, 50);
|
|
47
|
+
floor.observe(250, 50);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expect(floor.floor).toBeCloseTo(1_325, 5);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("forgets a room it has left, once the window has rolled", () => {
|
|
54
|
+
const floor = new RoomNoiseFloor();
|
|
55
|
+
feed(floor, 200, FULL_WINDOW_MS);
|
|
56
|
+
expect(floor.floor).toBeCloseTo(200, 5);
|
|
57
|
+
|
|
58
|
+
// Walk somewhere loud and stay there past the retention window.
|
|
59
|
+
feed(floor, 900, FULL_WINDOW_MS);
|
|
60
|
+
|
|
61
|
+
expect(floor.floor).toBeCloseTo(900, 5);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("weights by duration so an odd-sized tail cannot skew a block", () => {
|
|
65
|
+
const floor = new RoomNoiseFloor();
|
|
66
|
+
for (let block = 0; block < NOISE_FLOOR_BLOCK_COUNT; block += 1) {
|
|
67
|
+
floor.observe(100, NOISE_FLOOR_BLOCK_MS - 10);
|
|
68
|
+
floor.observe(10_000, 10);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 10ms of a loud tail moves a 1s block by ~1%, not by half.
|
|
72
|
+
expect(floor.floor).toBeCloseTo(
|
|
73
|
+
(100 * (NOISE_FLOOR_BLOCK_MS - 10) + 10_000 * 10) / NOISE_FLOOR_BLOCK_MS,
|
|
74
|
+
5,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("interrupt() drops the partial block without losing completed ones", () => {
|
|
79
|
+
const floor = new RoomNoiseFloor();
|
|
80
|
+
feed(floor, 300, FULL_WINDOW_MS);
|
|
81
|
+
// Half a block of near-silence, then playback starts and observation stops.
|
|
82
|
+
feed(floor, 10, NOISE_FLOOR_BLOCK_MS / 2);
|
|
83
|
+
floor.interrupt();
|
|
84
|
+
// Resume after playback; the two halves must not merge into one block that
|
|
85
|
+
// measures neither side of the gap.
|
|
86
|
+
feed(floor, 300, NOISE_FLOOR_BLOCK_MS);
|
|
87
|
+
|
|
88
|
+
expect(floor.floor).toBeCloseTo(300, 5);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("ignores non-positive durations and non-finite amplitudes", () => {
|
|
92
|
+
const floor = new RoomNoiseFloor();
|
|
93
|
+
floor.observe(500, 0);
|
|
94
|
+
floor.observe(500, -50);
|
|
95
|
+
floor.observe(Number.NaN, 50);
|
|
96
|
+
|
|
97
|
+
feed(floor, 300, FULL_WINDOW_MS);
|
|
98
|
+
expect(floor.floor).toBeCloseTo(300, 5);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("reset() clears completed blocks too", () => {
|
|
102
|
+
const floor = new RoomNoiseFloor();
|
|
103
|
+
feed(floor, 300, FULL_WINDOW_MS);
|
|
104
|
+
floor.reset();
|
|
105
|
+
|
|
106
|
+
expect(floor.floor).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rolling estimate of a room's continuous background noise level, on the same
|
|
3
|
+
* 16-bit mean-absolute-amplitude scale as `speech-energy.ts`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why an estimate is needed at all
|
|
6
|
+
*
|
|
7
|
+
* `detectPcm16SpeechActivity` compares a chunk against a fixed absolute
|
|
8
|
+
* threshold. A constant works only if every room delivers roughly the same
|
|
9
|
+
* silence, and rooms do not: a quiet home office and a cafe differ by more
|
|
10
|
+
* than the gap between silence and speech. Set the constant for the cafe and a
|
|
11
|
+
* soft talker is never heard; set it for the office and the cafe's background
|
|
12
|
+
* reads as continuous speech, which in a live-voice session means room noise
|
|
13
|
+
* repeatedly cancels the assistant mid-reply.
|
|
14
|
+
*
|
|
15
|
+
* ## Why the minimum of block means, and not an average
|
|
16
|
+
*
|
|
17
|
+
* The hard part is estimating background noise from a signal that also
|
|
18
|
+
* contains the user. Averaging fails: it folds the user's own speech into the
|
|
19
|
+
* floor, so the floor climbs while they talk and the gate goes deaf exactly
|
|
20
|
+
* when someone is using it. An envelope with a slow time constant fails the
|
|
21
|
+
* same way for the same reason, because "a person talking" and "a noisier
|
|
22
|
+
* room" both last about as long as each other, and a time constant cannot
|
|
23
|
+
* separate two things that differ only in level.
|
|
24
|
+
*
|
|
25
|
+
* What does separate them is that **speech is intermittent and room noise is
|
|
26
|
+
* continuous.** Any ten seconds of a real conversation contains at least one
|
|
27
|
+
* second in which nobody is speaking, and in that second the microphone hears
|
|
28
|
+
* the room and nothing else. So: average within a one-second block, then take
|
|
29
|
+
* the *minimum* across the retained blocks. Block-averaging first matters,
|
|
30
|
+
* because a minimum taken over raw chunks would latch onto the quietest 50 ms
|
|
31
|
+
* gap between two syllables, which is a pause inside speech, not the room.
|
|
32
|
+
*
|
|
33
|
+
* The estimate degrades honestly rather than dangerously. Ten unbroken seconds
|
|
34
|
+
* of speech (a monologue with no breath) raises the floor, because there was
|
|
35
|
+
* genuinely no quiet second to measure; it recovers on the first pause. The
|
|
36
|
+
* caller bounds what a raised floor is allowed to do.
|
|
37
|
+
*
|
|
38
|
+
* Pure and transport-neutral: amplitudes in, an estimate out, no session or
|
|
39
|
+
* provider state.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** Audio duration averaged into one block before it joins the estimate. */
|
|
43
|
+
export const NOISE_FLOOR_BLOCK_MS = 1_000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Completed blocks retained. Ten seconds is the shortest window that reliably
|
|
47
|
+
* contains a conversational pause, and short enough that walking somewhere
|
|
48
|
+
* quieter is reflected within a turn or two.
|
|
49
|
+
*/
|
|
50
|
+
export const NOISE_FLOOR_BLOCK_COUNT = 10;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tracks the quietest recent second of microphone audio.
|
|
54
|
+
*
|
|
55
|
+
* Feed it every chunk's mean amplitude while the near end is *not* playing
|
|
56
|
+
* audio back; feeding it during playback would teach it the assistant's own
|
|
57
|
+
* echo. Call {@link interrupt} when observation stops for that reason, so a
|
|
58
|
+
* block never averages across the gap.
|
|
59
|
+
*/
|
|
60
|
+
export class RoomNoiseFloor {
|
|
61
|
+
private readonly blockMs: number;
|
|
62
|
+
private readonly blockCount: number;
|
|
63
|
+
/** Completed block means, oldest first, at most `blockCount` of them. */
|
|
64
|
+
private readonly blocks: number[] = [];
|
|
65
|
+
/** Duration-weighted amplitude sum of the block being filled. */
|
|
66
|
+
private pendingWeightedSum = 0;
|
|
67
|
+
private pendingMs = 0;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
blockMs: number = NOISE_FLOOR_BLOCK_MS,
|
|
71
|
+
blockCount: number = NOISE_FLOOR_BLOCK_COUNT,
|
|
72
|
+
) {
|
|
73
|
+
this.blockMs = blockMs;
|
|
74
|
+
this.blockCount = blockCount;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fold one chunk's mean amplitude into the estimate.
|
|
79
|
+
*
|
|
80
|
+
* Weighted by duration so an irregular chunk size cannot skew a block: the
|
|
81
|
+
* client's framing is its own business, and a 10 ms tail must not count as
|
|
82
|
+
* much as the 50 ms frame before it.
|
|
83
|
+
*/
|
|
84
|
+
observe(meanAmplitude: number, durationMs: number): void {
|
|
85
|
+
if (!(durationMs > 0) || !Number.isFinite(meanAmplitude)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.pendingWeightedSum += meanAmplitude * durationMs;
|
|
89
|
+
this.pendingMs += durationMs;
|
|
90
|
+
if (this.pendingMs < this.blockMs) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.blocks.push(this.pendingWeightedSum / this.pendingMs);
|
|
94
|
+
if (this.blocks.length > this.blockCount) {
|
|
95
|
+
this.blocks.shift();
|
|
96
|
+
}
|
|
97
|
+
this.pendingWeightedSum = 0;
|
|
98
|
+
this.pendingMs = 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Drop the partially-filled block, keeping everything already completed.
|
|
103
|
+
*
|
|
104
|
+
* Call this when observation is about to pause (assistant playback starting)
|
|
105
|
+
* or resume after a gap. Without it a single block could average a half
|
|
106
|
+
* second of room from before the gap with a half second from after it, which
|
|
107
|
+
* is a measurement of neither.
|
|
108
|
+
*/
|
|
109
|
+
interrupt(): void {
|
|
110
|
+
this.pendingWeightedSum = 0;
|
|
111
|
+
this.pendingMs = 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The quietest retained second, or `null` until the window is **full**.
|
|
116
|
+
*
|
|
117
|
+
* Waiting for a full window is what makes the minimum safe. Over ten seconds
|
|
118
|
+
* the claim "at least one of these seconds contains no speech" holds; over
|
|
119
|
+
* one second it does not, and a partial window would happily report the first
|
|
120
|
+
* second of a sentence as the room's noise floor. That over-estimate is the
|
|
121
|
+
* dangerous direction: it raises the gate against the very voice it measured.
|
|
122
|
+
*
|
|
123
|
+
* `null` means "no opinion yet" and callers must fall back to their
|
|
124
|
+
* configured constant rather than treating it as a floor of zero. Since only
|
|
125
|
+
* non-playback audio is observed, the window fills with accumulated listening
|
|
126
|
+
* time rather than wall clock, which in a real session is a turn or two.
|
|
127
|
+
*/
|
|
128
|
+
get floor(): number | null {
|
|
129
|
+
if (this.blocks.length < this.blockCount) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return Math.min(...this.blocks);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Forget everything, e.g. when the input route changes under the session. */
|
|
136
|
+
reset(): void {
|
|
137
|
+
this.blocks.length = 0;
|
|
138
|
+
this.interrupt();
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/subagent/notify.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* pulling in the conversation/agent-loop core.
|
|
10
10
|
*
|
|
11
11
|
* Delivery targets the parent's in-process `Conversation` via the conversation
|
|
12
|
-
* registry: a notification is injected only when the parent is live here.
|
|
12
|
+
* registry: a notification is injected only when the parent is live here. A
|
|
13
|
+
* stale idle parent is rebuilt through `getOrCreateConversation` first so the
|
|
14
|
+
* injected turn uses the current provider, prompt, and credentials.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import {
|
|
@@ -36,14 +38,58 @@ export function injectMessageIntoParent(
|
|
|
36
38
|
message: string,
|
|
37
39
|
metadata?: Record<string, unknown>,
|
|
38
40
|
): void {
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
+
const existing = findConversation(parentConversationId);
|
|
42
|
+
if (!existing) {
|
|
41
43
|
log.warn(
|
|
42
44
|
{ parentConversationId },
|
|
43
45
|
"Subagent notification target parent conversation not found",
|
|
44
46
|
);
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
49
|
+
// A reload can mark the parent stale while children still run. Terminal
|
|
50
|
+
// injection is the first parent turn after the last child settles, so
|
|
51
|
+
// rebuild first when the instance is idle. Otherwise the completion turn
|
|
52
|
+
// would run on the construction-time provider, prompt, and credentials.
|
|
53
|
+
if (existing.isStale() && !existing.hasInFlightWork()) {
|
|
54
|
+
void import("../daemon/conversation-store.js")
|
|
55
|
+
.then(({ getOrCreateConversation }) =>
|
|
56
|
+
getOrCreateConversation(parentConversationId),
|
|
57
|
+
)
|
|
58
|
+
.then((parent) =>
|
|
59
|
+
deliverToParent(parent, parentConversationId, message, metadata),
|
|
60
|
+
)
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
log.error(
|
|
63
|
+
{ parentConversationId, err },
|
|
64
|
+
"Failed to rebuild stale parent before subagent notification",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
deliverToParent(existing, parentConversationId, message, metadata);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function deliverToParent(
|
|
73
|
+
parentConversation: {
|
|
74
|
+
enqueueMessage: (options: {
|
|
75
|
+
content: string;
|
|
76
|
+
metadata?: Record<string, unknown>;
|
|
77
|
+
isInteractive: boolean;
|
|
78
|
+
}) => { queued: boolean; rejected?: boolean };
|
|
79
|
+
persistUserMessage: (options: {
|
|
80
|
+
content: string;
|
|
81
|
+
metadata?: Record<string, unknown>;
|
|
82
|
+
}) => Promise<{ id: string }>;
|
|
83
|
+
runAgentLoop: (
|
|
84
|
+
message: string,
|
|
85
|
+
messageId: string,
|
|
86
|
+
options: { isInteractive: boolean },
|
|
87
|
+
) => Promise<unknown>;
|
|
88
|
+
},
|
|
89
|
+
parentConversationId: string,
|
|
90
|
+
message: string,
|
|
91
|
+
metadata?: Record<string, unknown>,
|
|
92
|
+
): void {
|
|
47
93
|
// Machine-injected with no human asserted present, so the notification
|
|
48
94
|
// turn runs non-interactive; it still streams to whoever is watching
|
|
49
95
|
// through the parent's sink.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { askQuestionTool } from "../ask-question/ask-question-tool.js";
|
|
4
|
+
import { reactToMessageTool } from "../channel/react-to-message.js";
|
|
4
5
|
import { fileEditTool } from "../filesystem/edit.js";
|
|
5
6
|
import { fileListTool } from "../filesystem/list.js";
|
|
6
7
|
import { fileReadTool } from "../filesystem/read.js";
|
|
@@ -140,6 +141,7 @@ describe("derived input_schema", () => {
|
|
|
140
141
|
hostShellTool,
|
|
141
142
|
requestSystemPermissionTool,
|
|
142
143
|
notifyParentTool,
|
|
144
|
+
reactToMessageTool,
|
|
143
145
|
];
|
|
144
146
|
|
|
145
147
|
test("every registered schema belongs to a tool whose input_schema is derived from it", () => {
|