@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d
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/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/openapi.yaml +74 -1
- package/package.json +1 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-guardian.test.ts +94 -58
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/computer-use-tools.test.ts +13 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/contacts-relay-reads.test.ts +13 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
- package/src/__tests__/guardian-dispatch.test.ts +95 -1
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-proxy.test.ts +299 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +167 -8
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +43 -0
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +189 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-bootstrap.test.ts +3 -73
- package/src/__tests__/plugin-route-contribution.test.ts +4 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
- package/src/__tests__/plugin-types.test.ts +0 -2
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/relay-server.test.ts +694 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +44 -13
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +2 -0
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/agent/loop-exclusive-tool.test.ts +150 -0
- package/src/agent/loop.ts +56 -0
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/llm-request-log-entry.ts +25 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +10 -8
- package/src/calls/inbound-trust-reader.ts +17 -1
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-server.ts +167 -50
- package/src/calls/relay-setup-router.ts +37 -7
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +175 -0
- package/src/cli/commands/plugins.ts +75 -3
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/loader.ts +36 -5
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +14 -5
- package/src/config/skills.ts +27 -5
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/contacts-write.ts +3 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/daemon/conversation-agent-loop.ts +9 -0
- package/src/daemon/conversation-process.ts +39 -17
- package/src/daemon/conversation-surfaces.ts +8 -0
- package/src/daemon/conversation-tool-setup.ts +49 -16
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +4 -34
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
- package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +78 -22
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/host-cu-proxy.ts +102 -11
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/conversation-crud.ts +38 -0
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +36 -502
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +58 -0
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +9 -8
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
- package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/schema-introspection.ts +14 -0
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +573 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +118 -0
- package/src/memory/worker-process.ts +72 -0
- package/src/notifications/__tests__/broadcaster.test.ts +16 -8
- package/src/notifications/__tests__/connected-channels.test.ts +114 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +68 -24
- package/src/notifications/emit-signal.ts +39 -14
- package/src/onboarding/checkin-event.test.ts +220 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +3 -5
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
- package/src/plugins/defaults/advisor/consult.ts +110 -6
- package/src/plugins/defaults/advisor/context-pack.ts +288 -0
- package/src/plugins/defaults/advisor/steering.ts +14 -2
- package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
- package/src/plugins/mtime-cache.ts +7 -2
- package/src/plugins/types.ts +0 -2
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +4 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/responses-provider.ts +5 -0
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +4 -0
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/retry.ts +4 -0
- package/src/providers/types.ts +9 -0
- package/src/providers/usage-tracking.ts +4 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
- package/src/runtime/access-request-helper.ts +19 -39
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/anchored-guardian.test.ts +156 -0
- package/src/runtime/anchored-guardian.ts +135 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +66 -7
- package/src/runtime/invite-redemption-service.ts +50 -18
- package/src/runtime/local-actor-identity.ts +76 -11
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +8 -32
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-routes.ts +74 -81
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +71 -0
- package/src/runtime/routes/mcp-auth-routes.ts +38 -15
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +51 -55
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +7 -9
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +85 -9
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/manager.ts +9 -0
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/computer-use/definitions.ts +8 -2
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +18 -0
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/types.ts +17 -2
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/platform.ts +5 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
|
@@ -326,3 +326,68 @@ describe("SubagentManager terminal disposal", () => {
|
|
|
326
326
|
asInternals(manager).stopSweep();
|
|
327
327
|
});
|
|
328
328
|
});
|
|
329
|
+
|
|
330
|
+
describe("SubagentManager.abort usage", () => {
|
|
331
|
+
test("emits the conversation's latest usage on abort, not zeros", () => {
|
|
332
|
+
const manager = new SubagentManager();
|
|
333
|
+
const sent: ServerMessage[] = [];
|
|
334
|
+
const sender = (msg: ServerMessage) => sent.push(msg);
|
|
335
|
+
|
|
336
|
+
const subagentId = "sa-abort-usage";
|
|
337
|
+
// state.usage starts at {0,0,0}; the live (fake) conversation has accrued
|
|
338
|
+
// usage (makeFakeConversation → {100, 50, 0.005}). Wire `sender` as the
|
|
339
|
+
// stored parent sender so `setStatus` routes the terminal event through it.
|
|
340
|
+
injectFakeSubagent(manager, subagentId, makeState(subagentId), sender);
|
|
341
|
+
|
|
342
|
+
const aborted = manager.abort(subagentId, sender, undefined, {
|
|
343
|
+
suppressNotification: true,
|
|
344
|
+
});
|
|
345
|
+
expect(aborted).toBe(true);
|
|
346
|
+
|
|
347
|
+
const statusMsg = sent.find(
|
|
348
|
+
(m): m is Extract<ServerMessage, { type: "subagent_status_changed" }> =>
|
|
349
|
+
m.type === "subagent_status_changed",
|
|
350
|
+
);
|
|
351
|
+
expect(statusMsg).toBeDefined();
|
|
352
|
+
expect(statusMsg!.status).toBe("aborted");
|
|
353
|
+
// The emitted usage is the conversation's accrued total — NOT the {0,0,0}
|
|
354
|
+
// init — so the client doesn't flush the token panel to zero on stop.
|
|
355
|
+
expect(statusMsg!.usage).toEqual({
|
|
356
|
+
inputTokens: 100,
|
|
357
|
+
outputTokens: 50,
|
|
358
|
+
estimatedCost: 0.005,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
asInternals(manager).stopSweep();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("keeps the last-known state.usage when the conversation was already released", () => {
|
|
365
|
+
const manager = new SubagentManager();
|
|
366
|
+
const sent: ServerMessage[] = [];
|
|
367
|
+
const sender = (msg: ServerMessage) => sent.push(msg);
|
|
368
|
+
|
|
369
|
+
const subagentId = "sa-abort-no-conv";
|
|
370
|
+
// No live conversation (released), but state carries a last-known usage —
|
|
371
|
+
// the abort must surface that, not overwrite it.
|
|
372
|
+
const state = makeState(subagentId, {
|
|
373
|
+
usage: { inputTokens: 320, outputTokens: 80, estimatedCost: 0.004 },
|
|
374
|
+
});
|
|
375
|
+
injectFakeSubagent(manager, subagentId, state, sender, null);
|
|
376
|
+
|
|
377
|
+
manager.abort(subagentId, sender, undefined, {
|
|
378
|
+
suppressNotification: true,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const statusMsg = sent.find(
|
|
382
|
+
(m): m is Extract<ServerMessage, { type: "subagent_status_changed" }> =>
|
|
383
|
+
m.type === "subagent_status_changed",
|
|
384
|
+
);
|
|
385
|
+
expect(statusMsg!.usage).toEqual({
|
|
386
|
+
inputTokens: 320,
|
|
387
|
+
outputTokens: 80,
|
|
388
|
+
estimatedCost: 0.004,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
asInternals(manager).stopSweep();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -4,6 +4,8 @@ import { describe, expect, mock, test } from "bun:test";
|
|
|
4
4
|
|
|
5
5
|
// Mock conversation-crud before importing tool executors that depend on it.
|
|
6
6
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
7
|
+
setConversationProcessingStartedAt: () => {},
|
|
8
|
+
isConversationProcessing: () => false,
|
|
7
9
|
setConversationOriginChannelIfUnset: () => {},
|
|
8
10
|
updateConversationContextWindow: () => {},
|
|
9
11
|
deleteMessageById: () => {},
|
|
@@ -3,6 +3,8 @@ import { mock } from "bun:test";
|
|
|
3
3
|
|
|
4
4
|
// Mock conversation-crud before importing tool executors that depend on it.
|
|
5
5
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
6
|
+
setConversationProcessingStartedAt: () => {},
|
|
7
|
+
isConversationProcessing: () => false,
|
|
6
8
|
setConversationOriginChannelIfUnset: () => {},
|
|
7
9
|
updateConversationContextWindow: () => {},
|
|
8
10
|
deleteMessageById: () => {},
|
|
@@ -29,6 +29,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
29
29
|
}),
|
|
30
30
|
}));
|
|
31
31
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
32
|
+
setConversationProcessingStartedAt: () => {},
|
|
33
|
+
isConversationProcessing: () => false,
|
|
32
34
|
setConversationOriginChannelIfUnset: () => {},
|
|
33
35
|
updateConversationContextWindow: () => {},
|
|
34
36
|
deleteMessageById: () => {},
|
|
@@ -50,6 +50,8 @@ const mockGetMessages = mock((_conversationId: string) => [
|
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
52
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
53
|
+
setConversationProcessingStartedAt: () => {},
|
|
54
|
+
isConversationProcessing: () => false,
|
|
53
55
|
getMessages: mockGetMessages,
|
|
54
56
|
getConversation: (_id: string) => null,
|
|
55
57
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
@@ -78,6 +78,8 @@ mock.module("../permissions/checker.js", () => ({
|
|
|
78
78
|
}));
|
|
79
79
|
|
|
80
80
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
81
|
+
setConversationProcessingStartedAt: () => {},
|
|
82
|
+
isConversationProcessing: () => false,
|
|
81
83
|
createConversation: (title: string) => ({ id: "conversation-1", title }),
|
|
82
84
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
83
85
|
}));
|
|
@@ -17,6 +17,7 @@ const mockConfig = {
|
|
|
17
17
|
shellDefaultTimeoutSec: 120,
|
|
18
18
|
shellMaxTimeoutSec: 600,
|
|
19
19
|
permissionTimeoutSec: 300,
|
|
20
|
+
questionResponseTimeoutSec: 1800,
|
|
20
21
|
},
|
|
21
22
|
sandbox: {
|
|
22
23
|
enabled: false,
|
|
@@ -1300,27 +1301,31 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1300
1301
|
|
|
1301
1302
|
describe("computePerToolTimeoutMs ask_question budget", () => {
|
|
1302
1303
|
// Regression guard: ask_question blocks on user input inside execute() via
|
|
1303
|
-
// QuestionPrompter, which waits up to
|
|
1304
|
-
// generic toolExecutionTimeoutSec wrapper must give ask_question a
|
|
1305
|
-
// strictly larger than that prompt timeout — otherwise the wrapper
|
|
1306
|
-
// first and orphans the still-pending prompt behind the confusing "may
|
|
1304
|
+
// QuestionPrompter, which waits up to questionResponseTimeoutSec. The
|
|
1305
|
+
// executor's generic toolExecutionTimeoutSec wrapper must give ask_question a
|
|
1306
|
+
// budget strictly larger than that prompt timeout — otherwise the wrapper
|
|
1307
|
+
// fires first and orphans the still-pending prompt behind the confusing "may
|
|
1307
1308
|
// still be running in the background" error. These assertions fail if the
|
|
1308
|
-
// special case is removed and ask_question falls back to the generic budget
|
|
1309
|
-
|
|
1310
|
-
|
|
1309
|
+
// special case is removed and ask_question falls back to the generic budget,
|
|
1310
|
+
// or if the executor budget and the prompter timeout drift onto different
|
|
1311
|
+
// config knobs.
|
|
1312
|
+
test("execution-timeout budget exceeds the prompt's own questionResponseTimeoutSec", () => {
|
|
1313
|
+
const { questionResponseTimeoutSec } = mockConfig.timeouts;
|
|
1311
1314
|
const askQuestionBudgetMs = computePerToolTimeoutMs("ask_question", {});
|
|
1312
1315
|
|
|
1313
|
-
expect(askQuestionBudgetMs).toBeGreaterThan(
|
|
1314
|
-
|
|
1316
|
+
expect(askQuestionBudgetMs).toBeGreaterThan(
|
|
1317
|
+
questionResponseTimeoutSec * 1000,
|
|
1318
|
+
);
|
|
1319
|
+
expect(askQuestionBudgetMs).toBe((questionResponseTimeoutSec + 5) * 1000);
|
|
1315
1320
|
});
|
|
1316
1321
|
|
|
1317
1322
|
test("the generic budget that would otherwise apply is shorter than the prompt timeout", () => {
|
|
1318
|
-
const {
|
|
1323
|
+
const { questionResponseTimeoutSec } = mockConfig.timeouts;
|
|
1319
1324
|
const genericBudgetMs = computePerToolTimeoutMs("some_other_tool", {});
|
|
1320
1325
|
|
|
1321
1326
|
// This is the collision the ask_question special case fixes: the generic
|
|
1322
1327
|
// execution-timeout budget is shorter than the prompter's own wait, so
|
|
1323
1328
|
// without the special case the wrapper trips first.
|
|
1324
|
-
expect(genericBudgetMs).toBeLessThan(
|
|
1329
|
+
expect(genericBudgetMs).toBeLessThan(questionResponseTimeoutSec * 1000);
|
|
1325
1330
|
});
|
|
1326
1331
|
});
|
|
@@ -60,6 +60,8 @@ const reserveMessageMock = mock(
|
|
|
60
60
|
const updateMessageContentMock = mock((_id: string, _content: string) => {});
|
|
61
61
|
|
|
62
62
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
63
|
+
setConversationProcessingStartedAt: () => {},
|
|
64
|
+
isConversationProcessing: () => false,
|
|
63
65
|
getConversation: () => null,
|
|
64
66
|
getMessageById: () => null,
|
|
65
67
|
updateMessageContent: updateMessageContentMock,
|
|
@@ -40,6 +40,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
42
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
43
|
+
setConversationProcessingStartedAt: () => {},
|
|
44
|
+
isConversationProcessing: () => false,
|
|
43
45
|
addMessage: () => ({ id: "mock-msg-id" }),
|
|
44
46
|
getMessageById: () => null,
|
|
45
47
|
updateMessageContent: () => {},
|
|
@@ -43,6 +43,8 @@ let mockedRowContent = "";
|
|
|
43
43
|
const updates: Array<{ id: string; content: string }> = [];
|
|
44
44
|
|
|
45
45
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
46
|
+
setConversationProcessingStartedAt: () => {},
|
|
47
|
+
isConversationProcessing: () => false,
|
|
46
48
|
addMessage: () => ({ id: "mock-msg-id" }),
|
|
47
49
|
getMessageById: (id: string) =>
|
|
48
50
|
mockedRowContent ? { id, content: mockedRowContent } : null,
|
|
@@ -335,7 +335,7 @@ describe("(b) prompt-path flow: confirmation_request bridges to guardian", () =>
|
|
|
335
335
|
};
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
-
test("trusted-contact confirmation_request emits guardian.question and creates delivery records", () => {
|
|
338
|
+
test("trusted-contact confirmation_request emits guardian.question and creates delivery records", async () => {
|
|
339
339
|
const canonicalRequest = createCanonicalGuardianRequest({
|
|
340
340
|
id: `req-bridge-${Date.now()}`,
|
|
341
341
|
kind: "tool_approval",
|
|
@@ -352,7 +352,7 @@ describe("(b) prompt-path flow: confirmation_request bridges to guardian", () =>
|
|
|
352
352
|
|
|
353
353
|
const trustContext = makeTrustedContactTrustContext();
|
|
354
354
|
|
|
355
|
-
const result = bridgeConfirmationRequestToGuardian({
|
|
355
|
+
const result = await bridgeConfirmationRequestToGuardian({
|
|
356
356
|
canonicalRequest,
|
|
357
357
|
trustContext,
|
|
358
358
|
conversationId: "conv-bridge-1",
|
|
@@ -371,7 +371,7 @@ describe("(b) prompt-path flow: confirmation_request bridges to guardian", () =>
|
|
|
371
371
|
expect(payload.requesterIdentifier).toBe("@requester");
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
-
test("bridge + tool_grant_request both use guardian.question for unified routing", () => {
|
|
374
|
+
test("bridge + tool_grant_request both use guardian.question for unified routing", async () => {
|
|
375
375
|
// The confirmation_request bridge and tool_grant_request helper both
|
|
376
376
|
// use 'guardian.question' as the notification signal, ensuring consistent
|
|
377
377
|
// guardian routing regardless of the approval path.
|
|
@@ -391,7 +391,7 @@ describe("(b) prompt-path flow: confirmation_request bridges to guardian", () =>
|
|
|
391
391
|
|
|
392
392
|
const trustContext = makeTrustedContactTrustContext();
|
|
393
393
|
|
|
394
|
-
bridgeConfirmationRequestToGuardian({
|
|
394
|
+
await bridgeConfirmationRequestToGuardian({
|
|
395
395
|
canonicalRequest,
|
|
396
396
|
trustContext,
|
|
397
397
|
conversationId: "conv-unified-1",
|
|
@@ -432,7 +432,7 @@ describe("(c) no-binding flow: trusted contact fails fast without guardian bindi
|
|
|
432
432
|
expect(state.promptWaitingAllowed).toBe(false);
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
-
test("bridge skips when no guardian binding exists for channel", () => {
|
|
435
|
+
test("bridge skips when no guardian binding exists for channel", async () => {
|
|
436
436
|
const canonicalRequest = createCanonicalGuardianRequest({
|
|
437
437
|
id: `req-nobinding-${Date.now()}`,
|
|
438
438
|
kind: "tool_approval",
|
|
@@ -449,7 +449,7 @@ describe("(c) no-binding flow: trusted contact fails fast without guardian bindi
|
|
|
449
449
|
|
|
450
450
|
const trustContext = makeTrustedContactTrustContext();
|
|
451
451
|
|
|
452
|
-
const result = bridgeConfirmationRequestToGuardian({
|
|
452
|
+
const result = await bridgeConfirmationRequestToGuardian({
|
|
453
453
|
canonicalRequest,
|
|
454
454
|
trustContext,
|
|
455
455
|
conversationId: "conv-nobinding",
|
|
@@ -543,7 +543,7 @@ describe("(d) unknown actor flow: fail-closed with no interactive approval", ()
|
|
|
543
543
|
expect(resolveRoutingState(withoutRoute).canBeInteractive).toBe(false);
|
|
544
544
|
});
|
|
545
545
|
|
|
546
|
-
test("bridge skips unknown actor sessions entirely", () => {
|
|
546
|
+
test("bridge skips unknown actor sessions entirely", async () => {
|
|
547
547
|
const canonicalRequest = createCanonicalGuardianRequest({
|
|
548
548
|
id: `req-unknown-${Date.now()}`,
|
|
549
549
|
kind: "tool_approval",
|
|
@@ -563,7 +563,7 @@ describe("(d) unknown actor flow: fail-closed with no interactive approval", ()
|
|
|
563
563
|
trustClass: "unknown",
|
|
564
564
|
};
|
|
565
565
|
|
|
566
|
-
const result = bridgeConfirmationRequestToGuardian({
|
|
566
|
+
const result = await bridgeConfirmationRequestToGuardian({
|
|
567
567
|
canonicalRequest,
|
|
568
568
|
trustContext,
|
|
569
569
|
conversationId: "conv-unknown",
|
|
@@ -965,7 +965,7 @@ describe("cross-milestone integration checks", () => {
|
|
|
965
965
|
);
|
|
966
966
|
});
|
|
967
967
|
|
|
968
|
-
test("M2+M4: bridge and tool_grant_request target the same guardian identity", () => {
|
|
968
|
+
test("M2+M4: bridge and tool_grant_request target the same guardian identity", async () => {
|
|
969
969
|
// Both the confirmation_request bridge (M2) and tool grant request escalation (M4)
|
|
970
970
|
// use the guardian binding's guardianExternalUserId to route notifications.
|
|
971
971
|
// Verify this consistency:
|
|
@@ -986,7 +986,7 @@ describe("cross-milestone integration checks", () => {
|
|
|
986
986
|
|
|
987
987
|
const trustContext = makeTrustedContactTrustContext();
|
|
988
988
|
|
|
989
|
-
const bridgeResult = bridgeConfirmationRequestToGuardian({
|
|
989
|
+
const bridgeResult = await bridgeConfirmationRequestToGuardian({
|
|
990
990
|
canonicalRequest,
|
|
991
991
|
trustContext,
|
|
992
992
|
conversationId: "conv-consistency",
|
|
@@ -109,6 +109,25 @@ mock.module("../calls/channel-admission-reader.js", () => ({
|
|
|
109
109
|
getChannelAdmissionPolicy: async () => mockAdmissionPolicy,
|
|
110
110
|
}));
|
|
111
111
|
|
|
112
|
+
// Mock the inbound trust reader used by the media-stream preflight. Captures
|
|
113
|
+
// its args so tests can assert the inbound caller's verdict is fetched, and
|
|
114
|
+
// returns mockInboundVerdict which is threaded into the preflight routeSetup.
|
|
115
|
+
let mockInboundVerdict: unknown = null;
|
|
116
|
+
let lastInboundVerdictArgs: Record<string, unknown> | null = null;
|
|
117
|
+
mock.module("../calls/inbound-trust-reader.js", () => ({
|
|
118
|
+
getInboundTrustVerdict: async (args: Record<string, unknown>) => {
|
|
119
|
+
lastInboundVerdictArgs = args;
|
|
120
|
+
return mockInboundVerdict;
|
|
121
|
+
},
|
|
122
|
+
getPhoneCallerVerdict: async (otherPartyNumber: string | undefined) => {
|
|
123
|
+
lastInboundVerdictArgs = {
|
|
124
|
+
channelType: "phone",
|
|
125
|
+
actorExternalId: otherPartyNumber || undefined,
|
|
126
|
+
};
|
|
127
|
+
return mockInboundVerdict;
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
|
|
112
131
|
mock.module("../config/env.js", () => ({
|
|
113
132
|
isHttpAuthDisabled: () => true,
|
|
114
133
|
getGatewayInternalBaseUrl: () => "http://gateway.internal:7830",
|
|
@@ -470,6 +489,8 @@ describe("twilio webhook routes", () => {
|
|
|
470
489
|
// Reset admission policy + captured routeSetup context between tests
|
|
471
490
|
mockAdmissionPolicy = null;
|
|
472
491
|
lastRouteSetupCtx = null;
|
|
492
|
+
mockInboundVerdict = null;
|
|
493
|
+
lastInboundVerdictArgs = null;
|
|
473
494
|
// Reset routeSetup mock to default normal_call
|
|
474
495
|
mockRouteSetupResult = {
|
|
475
496
|
outcome: { action: "normal_call", isInbound: true },
|
|
@@ -1269,6 +1290,81 @@ describe("twilio webhook routes", () => {
|
|
|
1269
1290
|
expect(lastRouteSetupCtx?.admissionPolicy).toBe("guardian_only");
|
|
1270
1291
|
});
|
|
1271
1292
|
|
|
1293
|
+
test("media-stream inbound: fetches the caller's verdict (From) and threads it into the preflight routeSetup", async () => {
|
|
1294
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1295
|
+
mockInboundVerdict = {
|
|
1296
|
+
channelType: "phone",
|
|
1297
|
+
actorExternalId: "+14155551234",
|
|
1298
|
+
contactId: "contact-1",
|
|
1299
|
+
channelId: "channel-1",
|
|
1300
|
+
status: "verified",
|
|
1301
|
+
policy: "allow",
|
|
1302
|
+
resolutionFailed: false,
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
const req = makeInboundVoiceRequest({
|
|
1306
|
+
CallSid: "CA_ms_verdict_inbound_1",
|
|
1307
|
+
From: "+14155551234",
|
|
1308
|
+
To: "+15550001111",
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
const res = await handleVoiceWebhook(req);
|
|
1312
|
+
expect(res.status).toBe(200);
|
|
1313
|
+
|
|
1314
|
+
// Inbound: verdict fetched for the caller (From) on the phone channel.
|
|
1315
|
+
expect(lastInboundVerdictArgs).toEqual({
|
|
1316
|
+
channelType: "phone",
|
|
1317
|
+
actorExternalId: "+14155551234",
|
|
1318
|
+
});
|
|
1319
|
+
// Verdict threaded into the preflight routeSetup.
|
|
1320
|
+
expect(lastRouteSetupCtx?.verdict).toEqual(mockInboundVerdict);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
test("media-stream inbound: a blocked/denied member verdict is classified deny in the preflight", async () => {
|
|
1324
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1325
|
+
mockInboundVerdict = {
|
|
1326
|
+
channelType: "phone",
|
|
1327
|
+
actorExternalId: "+14155551234",
|
|
1328
|
+
contactId: "contact-1",
|
|
1329
|
+
channelId: "channel-1",
|
|
1330
|
+
status: "blocked",
|
|
1331
|
+
policy: "deny",
|
|
1332
|
+
resolutionFailed: false,
|
|
1333
|
+
};
|
|
1334
|
+
// The real router returns `deny` for a blocked member verdict; the mock
|
|
1335
|
+
// reflects that outcome. deny is supported on media-stream, so the
|
|
1336
|
+
// preflight still emits Stream TwiML (denial spoken at stream start).
|
|
1337
|
+
mockRouteSetupResult = {
|
|
1338
|
+
outcome: {
|
|
1339
|
+
action: "deny",
|
|
1340
|
+
message:
|
|
1341
|
+
"This number is not authorized to reach the assistant right now.",
|
|
1342
|
+
logReason: "Inbound voice ACL: member blocked",
|
|
1343
|
+
},
|
|
1344
|
+
resolved: {
|
|
1345
|
+
assistantId: "self",
|
|
1346
|
+
isInbound: true,
|
|
1347
|
+
otherPartyNumber: "+14155551234",
|
|
1348
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1349
|
+
},
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
const req = makeInboundVoiceRequest({
|
|
1353
|
+
CallSid: "CA_ms_verdict_deny_1",
|
|
1354
|
+
From: "+14155551234",
|
|
1355
|
+
To: "+15550001111",
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
const res = await handleVoiceWebhook(req);
|
|
1359
|
+
expect(res.status).toBe(200);
|
|
1360
|
+
|
|
1361
|
+
// Verdict was threaded into routeSetup, which denied the caller.
|
|
1362
|
+
expect(lastRouteSetupCtx?.verdict).toEqual(mockInboundVerdict);
|
|
1363
|
+
const twiml = await res.text();
|
|
1364
|
+
expect(twiml).toContain("<Stream");
|
|
1365
|
+
expect(twiml).not.toContain("<ConversationRelay");
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1272
1368
|
test("media-stream: floor-denied caller classified deny still produces Stream TwiML (deny handled at stream level)", async () => {
|
|
1273
1369
|
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1274
1370
|
mockAdmissionPolicy = "guardian_only";
|
|
@@ -72,6 +72,8 @@ mock.module("../permissions/checker.js", () => ({
|
|
|
72
72
|
}));
|
|
73
73
|
|
|
74
74
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
75
|
+
setConversationProcessingStartedAt: () => {},
|
|
76
|
+
isConversationProcessing: () => false,
|
|
75
77
|
createConversation: (title: string) => ({ id: "conversation-1", title }),
|
|
76
78
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
77
79
|
}));
|
|
@@ -57,6 +57,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
57
57
|
}));
|
|
58
58
|
|
|
59
59
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
60
|
+
setConversationProcessingStartedAt: () => {},
|
|
61
|
+
isConversationProcessing: () => false,
|
|
60
62
|
addMessage: () => ({ id: "mock-msg-id" }),
|
|
61
63
|
getMessageById: () => null,
|
|
62
64
|
updateMessageContent: () => {},
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* and skipped (no silent provider-safe rewrite — operator must rename).
|
|
27
27
|
* - Multiple workspace tools register in a single batch.
|
|
28
28
|
*/
|
|
29
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
31
31
|
import { join } from "node:path";
|
|
32
32
|
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
@@ -42,7 +42,10 @@ import {
|
|
|
42
42
|
registerTool,
|
|
43
43
|
} from "../tools/registry.js";
|
|
44
44
|
import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
__resetWorkspaceToolCacheForTesting,
|
|
47
|
+
loadWorkspaceTools,
|
|
48
|
+
} from "../tools/workspace-tools/loader.js";
|
|
46
49
|
|
|
47
50
|
// Per-test counter so each writeTool() call lands in a unique tempdir,
|
|
48
51
|
// defeating bun's per-URL ESM cache between tests. Without this, a
|
|
@@ -87,6 +90,23 @@ function writeRemovedSentinel(name: string): void {
|
|
|
87
90
|
writeFileSync(join(toolsDir, `${name}.removed`), "");
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
/** Delete `<workspaceDir>/tools/<name><ext>` (defaults to `.ts`). */
|
|
94
|
+
function removeToolFile(name: string, ext = ".ts"): void {
|
|
95
|
+
rmSync(join(currentWorkspaceDir, "tools", `${name}${ext}`), { force: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Overwrite an existing tool file and bump its mtime into the future so the
|
|
100
|
+
* reconcile's mtime gate re-imports it even when the rewrite lands within
|
|
101
|
+
* the same millisecond as the original write.
|
|
102
|
+
*/
|
|
103
|
+
function rewriteTool(name: string, body: string, ext = ".ts"): void {
|
|
104
|
+
const path = join(currentWorkspaceDir, "tools", `${name}${ext}`);
|
|
105
|
+
writeFileSync(path, body);
|
|
106
|
+
const future = new Date(Date.now() + 5000);
|
|
107
|
+
utimesSync(path, future, future);
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
function makeFakeCoreTool(name: string): Tool {
|
|
91
111
|
return {
|
|
92
112
|
name,
|
|
@@ -94,6 +114,9 @@ function makeFakeCoreTool(name: string): Tool {
|
|
|
94
114
|
category: "test",
|
|
95
115
|
defaultRiskLevel: RiskLevel.Low,
|
|
96
116
|
executionTarget: "sandbox",
|
|
117
|
+
// Match the finalized shape the registry stores (defaults filled), so
|
|
118
|
+
// `getCoreToolOverride(name)` toEqual comparisons hold after registration.
|
|
119
|
+
exclusive: false,
|
|
97
120
|
input_schema: { type: "object", properties: {}, required: [] },
|
|
98
121
|
async execute(
|
|
99
122
|
_input: Record<string, unknown>,
|
|
@@ -133,6 +156,7 @@ export default {
|
|
|
133
156
|
describe("workspace tool loader", () => {
|
|
134
157
|
beforeEach(() => {
|
|
135
158
|
__clearRegistryForTesting();
|
|
159
|
+
__resetWorkspaceToolCacheForTesting();
|
|
136
160
|
freshWorkspace();
|
|
137
161
|
});
|
|
138
162
|
|
|
@@ -316,4 +340,173 @@ export default 42;
|
|
|
316
340
|
const names = getWorkspaceToolNames().sort();
|
|
317
341
|
expect(names).toEqual(["alpha", "beta", "gamma"]);
|
|
318
342
|
});
|
|
343
|
+
|
|
344
|
+
// ── Reconcile-on-read behavior ─────────────────────────────────────────
|
|
345
|
+
//
|
|
346
|
+
// loadWorkspaceTools() is idempotent and re-derives registry state from
|
|
347
|
+
// disk on every call. These cases cover the deltas a repeat call applies,
|
|
348
|
+
// which is what replaces the old filesystem watcher.
|
|
349
|
+
|
|
350
|
+
test("repeat call with no disk changes is a no-op (does not throw or duplicate)", async () => {
|
|
351
|
+
writeTool("stable_tool", WELL_FORMED_BODY);
|
|
352
|
+
|
|
353
|
+
await loadWorkspaceTools();
|
|
354
|
+
// A second reconcile must not throw on the already-registered name —
|
|
355
|
+
// the mtime cache recognizes the unchanged file and skips re-import.
|
|
356
|
+
await loadWorkspaceTools();
|
|
357
|
+
|
|
358
|
+
expect(getTool("stable_tool")).toBeDefined();
|
|
359
|
+
expect(getWorkspaceToolNames()).toEqual(["stable_tool"]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a file added after the first reconcile registers on the next", async () => {
|
|
363
|
+
writeTool("first", WELL_FORMED_BODY);
|
|
364
|
+
await loadWorkspaceTools();
|
|
365
|
+
expect(getWorkspaceToolNames()).toEqual(["first"]);
|
|
366
|
+
|
|
367
|
+
writeTool("second", WELL_FORMED_BODY);
|
|
368
|
+
await loadWorkspaceTools();
|
|
369
|
+
|
|
370
|
+
expect(getWorkspaceToolNames().sort()).toEqual(["first", "second"]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("a changed file is re-imported on the next reconcile", async () => {
|
|
374
|
+
writeTool("mutable", WELL_FORMED_BODY);
|
|
375
|
+
await loadWorkspaceTools();
|
|
376
|
+
expect(getTool("mutable")?.description).toBe("from workspace");
|
|
377
|
+
|
|
378
|
+
rewriteTool(
|
|
379
|
+
"mutable",
|
|
380
|
+
`
|
|
381
|
+
export default {
|
|
382
|
+
description: "edited in place",
|
|
383
|
+
defaultRiskLevel: "low",
|
|
384
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
385
|
+
async execute() {
|
|
386
|
+
return { content: "edited", isError: false };
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
`,
|
|
390
|
+
);
|
|
391
|
+
await loadWorkspaceTools();
|
|
392
|
+
|
|
393
|
+
expect(getTool("mutable")?.description).toBe("edited in place");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("a deleted net-new tool file is unregistered on the next reconcile", async () => {
|
|
397
|
+
writeTool("ephemeral", WELL_FORMED_BODY);
|
|
398
|
+
await loadWorkspaceTools();
|
|
399
|
+
expect(getTool("ephemeral")).toBeDefined();
|
|
400
|
+
|
|
401
|
+
removeToolFile("ephemeral");
|
|
402
|
+
await loadWorkspaceTools();
|
|
403
|
+
|
|
404
|
+
expect(getTool("ephemeral")).toBeUndefined();
|
|
405
|
+
expect(getWorkspaceToolNames()).toEqual([]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("deleting an override file restores the stashed core tool", async () => {
|
|
409
|
+
const core = makeFakeCoreTool("restore_me");
|
|
410
|
+
registerTool(core);
|
|
411
|
+
writeTool("restore_me", WELL_FORMED_BODY);
|
|
412
|
+
|
|
413
|
+
await loadWorkspaceTools();
|
|
414
|
+
expect(getToolOwner("restore_me")?.kind).toBe("workspace");
|
|
415
|
+
|
|
416
|
+
removeToolFile("restore_me");
|
|
417
|
+
await loadWorkspaceTools();
|
|
418
|
+
|
|
419
|
+
expect(getToolOwner("restore_me")).toBeUndefined();
|
|
420
|
+
expect(getTool("restore_me")).toEqual(core);
|
|
421
|
+
expect(getCoreToolOverride("restore_me")).toBeUndefined();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("deleting a .removed sentinel restores the stripped core tool", async () => {
|
|
425
|
+
const core = makeFakeCoreTool("strip_then_restore");
|
|
426
|
+
registerTool(core);
|
|
427
|
+
writeRemovedSentinel("strip_then_restore");
|
|
428
|
+
|
|
429
|
+
await loadWorkspaceTools();
|
|
430
|
+
expect(getTool("strip_then_restore")).toBeUndefined();
|
|
431
|
+
expect(getStrippedCoreToolNames()).toContain("strip_then_restore");
|
|
432
|
+
|
|
433
|
+
removeToolFile("strip_then_restore", ".removed");
|
|
434
|
+
await loadWorkspaceTools();
|
|
435
|
+
|
|
436
|
+
expect(getTool("strip_then_restore")).toEqual(core);
|
|
437
|
+
expect(getStrippedCoreToolNames()).not.toContain("strip_then_restore");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("the registered name is the filename stem, ignoring the file's own name field", async () => {
|
|
441
|
+
// The default export sets a different `name` — the loader must pin the
|
|
442
|
+
// registered name to the stem ("stem_wins") so the mtime cache and the
|
|
443
|
+
// unregister-on-delete path stay keyed by the same name.
|
|
444
|
+
writeTool(
|
|
445
|
+
"stem_wins",
|
|
446
|
+
`
|
|
447
|
+
export default {
|
|
448
|
+
name: "different_name",
|
|
449
|
+
description: "name field should be ignored",
|
|
450
|
+
defaultRiskLevel: "low",
|
|
451
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
452
|
+
async execute() {
|
|
453
|
+
return { content: "ok", isError: false };
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
`,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
await loadWorkspaceTools();
|
|
460
|
+
|
|
461
|
+
expect(getTool("stem_wins")).toBeDefined();
|
|
462
|
+
expect(getTool("different_name")).toBeUndefined();
|
|
463
|
+
expect(getWorkspaceToolNames()).toEqual(["stem_wins"]);
|
|
464
|
+
|
|
465
|
+
// Deleting the file unregisters by stem — no leaked "different_name".
|
|
466
|
+
removeToolFile("stem_wins");
|
|
467
|
+
await loadWorkspaceTools();
|
|
468
|
+
expect(getTool("stem_wins")).toBeUndefined();
|
|
469
|
+
expect(getTool("different_name")).toBeUndefined();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("per-tool isolation on reconcile: a bad file does not drop a valid edited tool", async () => {
|
|
473
|
+
writeTool("good_edit", WELL_FORMED_BODY);
|
|
474
|
+
await loadWorkspaceTools();
|
|
475
|
+
expect(getTool("good_edit")?.description).toBe("from workspace");
|
|
476
|
+
|
|
477
|
+
// Add a file that throws at import, and edit the good tool, in the same
|
|
478
|
+
// reconcile. The broken file must not prevent the edited tool from
|
|
479
|
+
// re-registering.
|
|
480
|
+
writeTool("broken_now", `throw new Error("boom at import");`);
|
|
481
|
+
rewriteTool(
|
|
482
|
+
"good_edit",
|
|
483
|
+
`
|
|
484
|
+
export default {
|
|
485
|
+
description: "edited and still here",
|
|
486
|
+
defaultRiskLevel: "low",
|
|
487
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
488
|
+
async execute() {
|
|
489
|
+
return { content: "ok", isError: false };
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
`,
|
|
493
|
+
);
|
|
494
|
+
await loadWorkspaceTools();
|
|
495
|
+
|
|
496
|
+
expect(getTool("broken_now")).toBeUndefined();
|
|
497
|
+
expect(getTool("good_edit")?.description).toBe("edited and still here");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("an edit that breaks an existing tool keeps the prior registration", async () => {
|
|
501
|
+
writeTool("was_good", WELL_FORMED_BODY);
|
|
502
|
+
await loadWorkspaceTools();
|
|
503
|
+
expect(getTool("was_good")?.description).toBe("from workspace");
|
|
504
|
+
|
|
505
|
+
// Rewrite the file into something that throws at import. The prior,
|
|
506
|
+
// working registration must stay in place rather than being torn down.
|
|
507
|
+
rewriteTool("was_good", `throw new Error("now broken");`);
|
|
508
|
+
await loadWorkspaceTools();
|
|
509
|
+
|
|
510
|
+
expect(getTool("was_good")?.description).toBe("from workspace");
|
|
511
|
+
});
|
|
319
512
|
});
|