@vellumai/assistant 0.11.8 → 0.11.9-staging.2
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 +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* validation, expiry checks, the atomic gateway CAS+outcome commit,
|
|
14
14
|
* kind-specific resolver dispatch, and grant minting.
|
|
15
15
|
*
|
|
16
|
-
* Routing reads (code lookup, pending discovery
|
|
17
|
-
*
|
|
16
|
+
* Routing reads (code lookup, pending discovery) use the degrading
|
|
17
|
+
* gateway-client variants: an unreachable gateway resolves to "no
|
|
18
18
|
* pending requests", so the message falls through to the normal pipeline
|
|
19
19
|
* instead of failing the inbound turn — decisions themselves still fail
|
|
20
20
|
* loudly inside the primitive.
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
* to allow for incremental migration and independent testability.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type { InboundReactionPayload } from "@vellumai/gateway-client";
|
|
27
26
|
import { isGuardianRequestExpired } from "@vellumai/gateway-client";
|
|
28
27
|
|
|
29
28
|
import {
|
|
@@ -38,7 +37,6 @@ import type {
|
|
|
38
37
|
import {
|
|
39
38
|
getGuardianRequestByCodeOrNull,
|
|
40
39
|
getGuardianRequestOrNull,
|
|
41
|
-
getPendingRequestByDestinationMessageOrNull,
|
|
42
40
|
type GuardianRequestWire,
|
|
43
41
|
listGuardianRequestsOrEmpty,
|
|
44
42
|
} from "../channels/gateway-guardian-requests.js";
|
|
@@ -62,7 +60,6 @@ import type {
|
|
|
62
60
|
ApprovalConversationGenerator,
|
|
63
61
|
} from "./http-types.js";
|
|
64
62
|
import * as pendingInteractions from "./pending-interactions.js";
|
|
65
|
-
import { reactionDecisionForEmoji } from "./routes/channel-route-shared.js";
|
|
66
63
|
|
|
67
64
|
const log = getLogger("guardian-reply-router");
|
|
68
65
|
|
|
@@ -94,29 +91,16 @@ export type GuardianPendingScope =
|
|
|
94
91
|
export interface GuardianReplyContext {
|
|
95
92
|
/** The raw message text (trimmed). */
|
|
96
93
|
messageText: string;
|
|
97
|
-
/** Source channel (telegram, whatsapp, etc.). */
|
|
98
|
-
channel: string;
|
|
99
94
|
/** Actor identity context for the sender. */
|
|
100
95
|
actor: ActorContext;
|
|
101
96
|
/**
|
|
102
97
|
* Conversation the message arrived in (may be the guardian's own). Only the
|
|
103
|
-
* text and NL paths read it;
|
|
104
|
-
*
|
|
98
|
+
* text and NL paths read it; `apr:` callbacks resolve their target by the
|
|
99
|
+
* embedded request id and may pass none.
|
|
105
100
|
*/
|
|
106
101
|
conversationId?: string;
|
|
107
102
|
/** Callback data from button presses (e.g. `apr:<requestId>:<action>`). */
|
|
108
103
|
callbackData?: string;
|
|
109
|
-
/**
|
|
110
|
-
* Structured reaction payload when the inbound event is a reaction; the
|
|
111
|
-
* reaction branch keys on it and reads nothing off callbackData.
|
|
112
|
-
*/
|
|
113
|
-
reaction?: InboundReactionPayload;
|
|
114
|
-
/**
|
|
115
|
-
* For emoji-reaction decisions: the
|
|
116
|
-
* channel-native id (e.g. Slack `ts`) of the message the reaction was
|
|
117
|
-
* attached to. Used to recover the target request from its delivery record.
|
|
118
|
-
*/
|
|
119
|
-
reactedMessageTs?: string;
|
|
120
104
|
/**
|
|
121
105
|
* How to scope this guardian's pending requests (see {@link
|
|
122
106
|
* GuardianPendingScope}). Omitted is equivalent to
|
|
@@ -357,69 +341,14 @@ export async function routeGuardianReply(
|
|
|
357
341
|
): Promise<GuardianReplyResult> {
|
|
358
342
|
const {
|
|
359
343
|
messageText,
|
|
360
|
-
channel,
|
|
361
344
|
actor,
|
|
362
345
|
conversationId,
|
|
363
346
|
callbackData,
|
|
364
|
-
reactedMessageTs,
|
|
365
347
|
approvalConversationGenerator,
|
|
366
348
|
channelDeliveryContext,
|
|
367
349
|
emissionContext,
|
|
368
350
|
} = ctx;
|
|
369
351
|
|
|
370
|
-
// ── 0. Reaction decisions (emoji on a delivered approval card) ──
|
|
371
|
-
// A reaction carries an emoji plus the message it is attached to. Map the
|
|
372
|
-
// emoji to an action and recover the target request from that card's
|
|
373
|
-
// delivery record. Addressing by the reacted message disambiguates precisely
|
|
374
|
-
// even when several cards are pending in the same chat, so — unlike the
|
|
375
|
-
// text/NL paths — no clarification prompt is ever needed. `reaction_removed`
|
|
376
|
-
// never expresses intent and is filtered out before reaching the router.
|
|
377
|
-
if (ctx.reaction?.op === "added") {
|
|
378
|
-
const decision = reactionDecisionForEmoji(ctx.reaction.emoji);
|
|
379
|
-
const guardianChatId = channelDeliveryContext?.guardianChatId;
|
|
380
|
-
if (!decision || !reactedMessageTs || !guardianChatId) {
|
|
381
|
-
// Unknown emoji, or missing addressing context — not an actionable
|
|
382
|
-
// approval reaction. Leave it for the caller to persist as a transcript
|
|
383
|
-
// signal (it must not trigger an agent turn).
|
|
384
|
-
return notConsumed();
|
|
385
|
-
}
|
|
386
|
-
// A delivery row keys its card by the destination the adapter delivered
|
|
387
|
-
// to, and channels address that destination in one of two modalities: a
|
|
388
|
-
// chat (the conversation the reaction arrives from) or a person (the
|
|
389
|
-
// guardian's own user id, which person-addressed adapters store so a
|
|
390
|
-
// stale room id can never become a delivery target). The reactor on an
|
|
391
|
-
// approval card is the person it was delivered to, so their id is the
|
|
392
|
-
// second probe; identity validation in applyGuardianDecision still
|
|
393
|
-
// guards the decision itself.
|
|
394
|
-
const request =
|
|
395
|
-
(await getPendingRequestByDestinationMessageOrNull(
|
|
396
|
-
channel,
|
|
397
|
-
guardianChatId,
|
|
398
|
-
reactedMessageTs,
|
|
399
|
-
)) ??
|
|
400
|
-
(actor.actorExternalUserId
|
|
401
|
-
? await getPendingRequestByDestinationMessageOrNull(
|
|
402
|
-
channel,
|
|
403
|
-
actor.actorExternalUserId,
|
|
404
|
-
reactedMessageTs,
|
|
405
|
-
)
|
|
406
|
-
: null);
|
|
407
|
-
if (!request) {
|
|
408
|
-
// The reacted message is not a known pending approval card (a stray
|
|
409
|
-
// reaction, or one whose request was already resolved). Never approve
|
|
410
|
-
// off an unrecognized message.
|
|
411
|
-
return notConsumed();
|
|
412
|
-
}
|
|
413
|
-
return applyDecision(
|
|
414
|
-
request.id,
|
|
415
|
-
decision.action,
|
|
416
|
-
actor,
|
|
417
|
-
undefined,
|
|
418
|
-
channelDeliveryContext,
|
|
419
|
-
emissionContext,
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
352
|
const pendingScope: GuardianPendingScope = ctx.pendingScope ?? {
|
|
424
353
|
mode: "identity-fallback",
|
|
425
354
|
};
|
|
@@ -202,7 +202,7 @@ function compileRoute(def: HTTPRouteDefinition): CompiledRoute {
|
|
|
202
202
|
const typePattern = PARAM_TYPE_PATTERNS[paramTypeMap.get(name) ?? ""];
|
|
203
203
|
return typePattern ? `(${typePattern})` : "([^/]+)";
|
|
204
204
|
}
|
|
205
|
-
return
|
|
205
|
+
return RegExp.escape(segment);
|
|
206
206
|
})
|
|
207
207
|
.join("\\/");
|
|
208
208
|
|
|
@@ -210,7 +210,3 @@ function compileRoute(def: HTTPRouteDefinition): CompiledRoute {
|
|
|
210
210
|
|
|
211
211
|
return { def, regex, paramNames };
|
|
212
212
|
}
|
|
213
|
-
|
|
214
|
-
function escapeRegex(str: string): string {
|
|
215
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
216
|
-
}
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Mirrors `confirmation-request-guardian-bridge.ts` for the question kind:
|
|
6
6
|
* emits the notification signal (the broadcaster renders the options as card
|
|
7
|
-
* actions — see `
|
|
8
|
-
* card deliveries so reply routing can address taps
|
|
9
|
-
* request.
|
|
7
|
+
* actions — see `resolveQuestionContext`) and records the resulting
|
|
8
|
+
* card deliveries so reply routing can address taps back to the request.
|
|
10
9
|
*
|
|
11
10
|
* Unlike the confirmation bridge — which escalates a NON-guardian's request TO
|
|
12
11
|
* the guardian — the caller (`question-guardian-request.ts`) has already
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `assistant-initiated-threads` split: the threads the assistant started
|
|
3
|
+
* on its own (`source = 'assistant_initiated'`, opt-in at creation) become
|
|
4
|
+
* their own sidebar section instead of sitting in Chats.
|
|
5
|
+
*
|
|
6
|
+
* The flag is the whole point of these tests. It is a read-side gate over rows
|
|
7
|
+
* that exist either way, so the off arm must be byte-identical to the behavior
|
|
8
|
+
* that shipped before the section existed — that is what makes the feature
|
|
9
|
+
* reversible without a migration. Both arms are asserted for every claim.
|
|
10
|
+
*
|
|
11
|
+
* The gate module is mocked rather than the flag registry: the registry
|
|
12
|
+
* resolves through workspace override files, and a test that wrote those would
|
|
13
|
+
* be asserting the resolver's behavior rather than this route's.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
17
|
+
|
|
18
|
+
// Avoid spinning up the real event hub for the pinned/groups branches.
|
|
19
|
+
mock.module("../../assistant-event-hub.js", () => ({
|
|
20
|
+
assistantEventHub: {
|
|
21
|
+
publish: async () => {},
|
|
22
|
+
subscribe: () => () => {},
|
|
23
|
+
},
|
|
24
|
+
broadcastMessage: () => {},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
let flagEnabled = false;
|
|
28
|
+
mock.module("../../../config/assistant-initiated-threads-gate.js", () => ({
|
|
29
|
+
isAssistantInitiatedThreadsEnabled: () => flagEnabled,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import { projectAssistantMessage } from "../../../persistence/conversation-attention-store.js";
|
|
33
|
+
import { createConversation } from "../../../persistence/conversation-crud.js";
|
|
34
|
+
import { getDb } from "../../../persistence/db-connection.js";
|
|
35
|
+
import { initializeDb } from "../../../persistence/db-init.js";
|
|
36
|
+
import { createGroup } from "../../../persistence/group-crud.js";
|
|
37
|
+
import { rawRun } from "../../../persistence/raw-query.js";
|
|
38
|
+
import { conversations } from "../../../persistence/schema/index.js";
|
|
39
|
+
import { ROUTES as CONVERSATION_LIST_ROUTES } from "../conversation-list-routes.js";
|
|
40
|
+
import type { RouteDefinition } from "../types.js";
|
|
41
|
+
|
|
42
|
+
await initializeDb();
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Helpers
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function findHandler(routes: RouteDefinition[], operationId: string) {
|
|
49
|
+
const route = routes.find((r) => r.operationId === operationId);
|
|
50
|
+
if (!route) {
|
|
51
|
+
throw new Error(`Route ${operationId} not found`);
|
|
52
|
+
}
|
|
53
|
+
return route.handler;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sectionsHandler = findHandler(
|
|
57
|
+
CONVERSATION_LIST_ROUTES,
|
|
58
|
+
"getConversationSections",
|
|
59
|
+
);
|
|
60
|
+
const listHandler = findHandler(CONVERSATION_LIST_ROUTES, "listConversations");
|
|
61
|
+
|
|
62
|
+
interface SectionRow {
|
|
63
|
+
kind: "pinned" | "group" | "channel" | "chats" | "assistant";
|
|
64
|
+
channelId?: string;
|
|
65
|
+
groupId?: string;
|
|
66
|
+
total: number;
|
|
67
|
+
unread: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function invokeSections(): SectionRow[] {
|
|
71
|
+
return (sectionsHandler({}) as { sections: SectionRow[] }).sections;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ListResponse {
|
|
75
|
+
conversations: Array<{ id: string; title: string }>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function invokeList(queryParams: Record<string, string> = {}): ListResponse {
|
|
79
|
+
return listHandler({ queryParams }) as ListResponse;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function titlesFrom(response: ListResponse): string[] {
|
|
83
|
+
return response.conversations.map((c) => c.title).sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A thread a producer opted into the assistant-initiated section: a
|
|
88
|
+
* standard-type conversation, filed nowhere, stamped
|
|
89
|
+
* `source: 'assistant_initiated'` at creation (ASSISTANT_INITIATED_SOURCE).
|
|
90
|
+
*/
|
|
91
|
+
function seedAssistantInitiated(title: string): string {
|
|
92
|
+
return createConversation({ title, source: "assistant_initiated" }).id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A transactional notification trail - what the guardian request flows and
|
|
97
|
+
* channel deliveries materialize (`source: 'notification'`, see
|
|
98
|
+
* `notifications/conversation-pairing.ts`). Never a member of the section:
|
|
99
|
+
* these belong to the bell and to Chats.
|
|
100
|
+
*/
|
|
101
|
+
function seedNotificationTrail(title: string): string {
|
|
102
|
+
return createConversation({ title, source: "notification" }).id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function seedUnseen(conversationId: string): void {
|
|
106
|
+
projectAssistantMessage({
|
|
107
|
+
conversationId,
|
|
108
|
+
messageId: `msg-${conversationId}`,
|
|
109
|
+
messageAt: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stampChannel(conversationId: string, channel: string): void {
|
|
114
|
+
rawRun(
|
|
115
|
+
"test:stampChannel",
|
|
116
|
+
"UPDATE conversations SET origin_channel = ? WHERE id = ?",
|
|
117
|
+
channel,
|
|
118
|
+
conversationId,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pin(conversationId: string): void {
|
|
123
|
+
rawRun(
|
|
124
|
+
"test:pinConversation",
|
|
125
|
+
"UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
|
|
126
|
+
conversationId,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fileIntoGroup(conversationId: string, groupId: string): void {
|
|
131
|
+
rawRun(
|
|
132
|
+
"test:fileIntoGroup",
|
|
133
|
+
"UPDATE conversations SET group_id = ? WHERE id = ?",
|
|
134
|
+
groupId,
|
|
135
|
+
conversationId,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
flagEnabled = false;
|
|
141
|
+
getDb().delete(conversations).run();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Flag off — the shipped behavior, unchanged
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
describe("assistant-initiated threads — flag off", () => {
|
|
149
|
+
test("emits no assistant section, and the threads stay counted in Chats", () => {
|
|
150
|
+
seedAssistantInitiated("overnight-realization");
|
|
151
|
+
createConversation({ title: "user-thread" });
|
|
152
|
+
|
|
153
|
+
const sections = invokeSections();
|
|
154
|
+
|
|
155
|
+
expect(sections.some((s) => s.kind === "assistant")).toBe(false);
|
|
156
|
+
expect(sections).toContainEqual({ kind: "chats", total: 2, unread: 0 });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("the threads stay listed in the standard list", () => {
|
|
160
|
+
seedAssistantInitiated("overnight-realization");
|
|
161
|
+
createConversation({ title: "user-thread" });
|
|
162
|
+
|
|
163
|
+
expect(titlesFrom(invokeList())).toEqual([
|
|
164
|
+
"overnight-realization",
|
|
165
|
+
"user-thread",
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("their unread still counts toward Chats", () => {
|
|
170
|
+
seedUnseen(seedAssistantInitiated("noticed-something"));
|
|
171
|
+
createConversation({ title: "user-thread" });
|
|
172
|
+
|
|
173
|
+
expect(invokeSections()).toContainEqual({
|
|
174
|
+
kind: "chats",
|
|
175
|
+
total: 2,
|
|
176
|
+
unread: 1,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Flag on — the split
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
describe("assistant-initiated threads — flag on", () => {
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
flagEnabled = true;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("the threads move out of Chats into their own section", () => {
|
|
191
|
+
seedAssistantInitiated("overnight-realization");
|
|
192
|
+
seedAssistantInitiated("noticed-something");
|
|
193
|
+
createConversation({ title: "user-thread" });
|
|
194
|
+
|
|
195
|
+
const sections = invokeSections();
|
|
196
|
+
|
|
197
|
+
expect(sections).toContainEqual({ kind: "assistant", total: 2, unread: 0 });
|
|
198
|
+
expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 0 });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("the section renders at zero, the way Chats does", () => {
|
|
202
|
+
createConversation({ title: "user-thread" });
|
|
203
|
+
|
|
204
|
+
expect(invokeSections()).toContainEqual({
|
|
205
|
+
kind: "assistant",
|
|
206
|
+
total: 0,
|
|
207
|
+
unread: 0,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("unread follows the section, and is not double-counted in Chats", () => {
|
|
212
|
+
seedUnseen(seedAssistantInitiated("noticed-something"));
|
|
213
|
+
seedAssistantInitiated("read-already");
|
|
214
|
+
seedUnseen(createConversation({ title: "user-thread" }).id);
|
|
215
|
+
|
|
216
|
+
const sections = invokeSections();
|
|
217
|
+
|
|
218
|
+
expect(sections).toContainEqual({ kind: "assistant", total: 2, unread: 1 });
|
|
219
|
+
expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 1 });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("the split leaves channel sections alone", () => {
|
|
223
|
+
seedAssistantInitiated("overnight-realization");
|
|
224
|
+
stampChannel(createConversation({ title: "slack-thread" }).id, "slack");
|
|
225
|
+
|
|
226
|
+
const sections = invokeSections();
|
|
227
|
+
|
|
228
|
+
expect(sections).toContainEqual({ kind: "assistant", total: 1, unread: 0 });
|
|
229
|
+
expect(sections).toContainEqual({
|
|
230
|
+
kind: "channel",
|
|
231
|
+
channelId: "slack",
|
|
232
|
+
total: 1,
|
|
233
|
+
unread: 0,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("groupId=system:assistant lists exactly those threads", () => {
|
|
238
|
+
seedAssistantInitiated("overnight-realization");
|
|
239
|
+
seedAssistantInitiated("noticed-something");
|
|
240
|
+
createConversation({ title: "user-thread" });
|
|
241
|
+
|
|
242
|
+
expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
|
|
243
|
+
"noticed-something",
|
|
244
|
+
"overnight-realization",
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("the standard list withholds them, and keeps every NULL-source row", () => {
|
|
249
|
+
// The regression this guards: `NOT (source = '...')` is NULL for the
|
|
250
|
+
// NULL-source rows that make up most of the list, so a naive negation
|
|
251
|
+
// would return an empty list rather than one section fewer.
|
|
252
|
+
seedAssistantInitiated("overnight-realization");
|
|
253
|
+
createConversation({ title: "user-thread" });
|
|
254
|
+
createConversation({ title: "another-user-thread" });
|
|
255
|
+
|
|
256
|
+
expect(titlesFrom(invokeList())).toEqual([
|
|
257
|
+
"another-user-thread",
|
|
258
|
+
"user-thread",
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a list already scoped to the section is not narrowed to nothing", () => {
|
|
263
|
+
// `groupId=system:assistant` selects on the same column the exclusion
|
|
264
|
+
// negates, so applying both would return an empty page for the one caller
|
|
265
|
+
// that actually wants these rows.
|
|
266
|
+
seedAssistantInitiated("overnight-realization");
|
|
267
|
+
|
|
268
|
+
expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
|
|
269
|
+
"overnight-realization",
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("a pinned thread belongs to Pinned, not to both", () => {
|
|
274
|
+
// Section membership is single-valued everywhere else in the sidebar, and
|
|
275
|
+
// this section is carved out of the same ungrouped set Chats is, so
|
|
276
|
+
// pinning has to move a thread rather than duplicate it.
|
|
277
|
+
pin(seedAssistantInitiated("pinned-realization"));
|
|
278
|
+
seedAssistantInitiated("ordinary-realization");
|
|
279
|
+
|
|
280
|
+
const sections = invokeSections();
|
|
281
|
+
|
|
282
|
+
expect(sections).toContainEqual({ kind: "assistant", total: 1, unread: 0 });
|
|
283
|
+
expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
|
|
284
|
+
expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
|
|
285
|
+
"ordinary-realization",
|
|
286
|
+
]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("transactional notification trails are not members, and stay in Chats", () => {
|
|
290
|
+
// The membership source is opt-in precisely so the flag turning on cannot
|
|
291
|
+
// sweep the historical guardian-request and channel-delivery trails
|
|
292
|
+
// (`source = 'notification'`) into the section: only threads written for
|
|
293
|
+
// the section, from the moment producers start stamping its source, are
|
|
294
|
+
// members.
|
|
295
|
+
seedNotificationTrail("approve-this-call");
|
|
296
|
+
seedAssistantInitiated("overnight-realization");
|
|
297
|
+
|
|
298
|
+
const sections = invokeSections();
|
|
299
|
+
|
|
300
|
+
expect(sections).toContainEqual({ kind: "assistant", total: 1, unread: 0 });
|
|
301
|
+
expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 0 });
|
|
302
|
+
expect(titlesFrom(invokeList())).toEqual(["approve-this-call"]);
|
|
303
|
+
expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
|
|
304
|
+
"overnight-realization",
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("a thread filed into a custom group leaves the section and stays listable", () => {
|
|
309
|
+
// The exclusion is the complement of the section, so a thread that moved
|
|
310
|
+
// out of the section must not stay excluded from the list of the group it
|
|
311
|
+
// moved into — otherwise filing one makes it unreachable.
|
|
312
|
+
const group = createGroup("Reading List");
|
|
313
|
+
const filed = seedAssistantInitiated("filed-realization");
|
|
314
|
+
fileIntoGroup(filed, group.id);
|
|
315
|
+
|
|
316
|
+
expect(invokeSections()).toContainEqual({
|
|
317
|
+
kind: "assistant",
|
|
318
|
+
total: 0,
|
|
319
|
+
unread: 0,
|
|
320
|
+
});
|
|
321
|
+
expect(titlesFrom(invokeList({ groupId: group.id }))).toEqual([
|
|
322
|
+
"filed-realization",
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("the Archive view keeps archived section threads", () => {
|
|
327
|
+
// Archive asks archiveStatus=archived with no groupId, and the section
|
|
328
|
+
// only ever lists active rows, so the split's exclusion must not reach
|
|
329
|
+
// archived reads or an archived section thread is visible nowhere.
|
|
330
|
+
const archived = seedAssistantInitiated("archived-realization");
|
|
331
|
+
rawRun(
|
|
332
|
+
"test:archive",
|
|
333
|
+
"UPDATE conversations SET archived_at = ? WHERE id = ?",
|
|
334
|
+
Date.now(),
|
|
335
|
+
archived,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(titlesFrom(invokeList({ archiveStatus: "archived" }))).toEqual([
|
|
339
|
+
"archived-realization",
|
|
340
|
+
]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("the background umbrella is left unnarrowed", () => {
|
|
344
|
+
// A background row carrying the section's own source is addressed by
|
|
345
|
+
// conversation type, and narrowing that bucket by source would drop rows
|
|
346
|
+
// its callers still page through.
|
|
347
|
+
const background = createConversation({
|
|
348
|
+
title: "background-realization",
|
|
349
|
+
conversationType: "background",
|
|
350
|
+
source: "assistant_initiated",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(
|
|
354
|
+
invokeList({ conversationType: "background" }).conversations.map(
|
|
355
|
+
(c) => c.id,
|
|
356
|
+
),
|
|
357
|
+
).toContain(background.id);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command mode of `POST /v1/dictation`: selected text with the words spoken
|
|
3
|
+
* over it. The model is asked, through one forced tool call, whether the
|
|
4
|
+
* words wanted the selection changed or asked about it, and the route answers
|
|
5
|
+
* `command` with the edit or `question` with the words as they were.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
type ToolInput = { kind?: string; text?: string };
|
|
11
|
+
let nextToolInput: ToolInput | null = null;
|
|
12
|
+
let forcedTool: string | undefined;
|
|
13
|
+
|
|
14
|
+
mock.module("../../../providers/provider-send-message.js", () => ({
|
|
15
|
+
getConfiguredProvider: async () => ({
|
|
16
|
+
sendMessage: async (
|
|
17
|
+
_messages: unknown,
|
|
18
|
+
options: { config?: { tool_choice?: { name?: string } } },
|
|
19
|
+
) => {
|
|
20
|
+
forcedTool = options.config?.tool_choice?.name;
|
|
21
|
+
return {
|
|
22
|
+
content: nextToolInput
|
|
23
|
+
? [
|
|
24
|
+
{
|
|
25
|
+
type: "tool_use",
|
|
26
|
+
id: "tool-1",
|
|
27
|
+
name: "transform_selection",
|
|
28
|
+
input: nextToolInput,
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
: [{ type: "text", text: "Sure, here it is." }],
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
extractToolUse: (response: { content: Array<{ type: string }> }) =>
|
|
36
|
+
response.content.find((block) => block.type === "tool_use"),
|
|
37
|
+
userMessage: (text: string) => ({ role: "user", content: text }),
|
|
38
|
+
createTimeout: () => ({ signal: undefined, cleanup: () => undefined }),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
mock.module("../../../daemon/dictation-profile-store.js", () => ({
|
|
42
|
+
resolveProfile: () => ({
|
|
43
|
+
profile: { id: "default", stylePrompt: "", dictionary: [], snippets: [] },
|
|
44
|
+
source: "default",
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
mock.module("../../../daemon/dictation-text-processing.js", () => ({
|
|
49
|
+
applyDictionary: (text: string) => text,
|
|
50
|
+
expandSnippets: (text: string) => text,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const { ROUTES } = await import("../diagnostics-routes.js");
|
|
54
|
+
const route = ROUTES.find((r) => r.operationId === "dictation_post")!;
|
|
55
|
+
|
|
56
|
+
const SELECTION = "Please send me the files.";
|
|
57
|
+
|
|
58
|
+
async function speakOverSelection(transcription: string) {
|
|
59
|
+
return (await route.handler({
|
|
60
|
+
body: {
|
|
61
|
+
transcription,
|
|
62
|
+
context: {
|
|
63
|
+
bundleIdentifier: "com.example.mail",
|
|
64
|
+
appName: "Example Mail",
|
|
65
|
+
windowTitle: "New Message",
|
|
66
|
+
selectedText: SELECTION,
|
|
67
|
+
cursorInTextField: true,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
} as unknown as Parameters<typeof route.handler>[0])) as {
|
|
71
|
+
text: string;
|
|
72
|
+
mode: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
nextToolInput = null;
|
|
78
|
+
forcedTool = undefined;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("dictation command mode", () => {
|
|
82
|
+
test("answers an instruction with the edit", async () => {
|
|
83
|
+
nextToolInput = { kind: "edit", text: "Could you send the files over?" };
|
|
84
|
+
|
|
85
|
+
const result = await speakOverSelection("make this friendlier");
|
|
86
|
+
|
|
87
|
+
expect(forcedTool).toBe("transform_selection");
|
|
88
|
+
expect(result).toMatchObject({
|
|
89
|
+
mode: "command",
|
|
90
|
+
text: "Could you send the files over?",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("answers a question with the words as they were", async () => {
|
|
95
|
+
nextToolInput = { kind: "answer", text: "" };
|
|
96
|
+
|
|
97
|
+
const result = await speakOverSelection("what does this mean");
|
|
98
|
+
|
|
99
|
+
expect(result).toMatchObject({
|
|
100
|
+
mode: "question",
|
|
101
|
+
text: "what does this mean",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("an edit with nothing in it is a question", async () => {
|
|
106
|
+
nextToolInput = { kind: "edit", text: " " };
|
|
107
|
+
|
|
108
|
+
const result = await speakOverSelection("is this right");
|
|
109
|
+
|
|
110
|
+
expect(result.mode).toBe("question");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a model that answers in prose hands the selection back unchanged", async () => {
|
|
114
|
+
nextToolInput = null;
|
|
115
|
+
|
|
116
|
+
const result = await speakOverSelection("make this friendlier");
|
|
117
|
+
|
|
118
|
+
expect(result).toMatchObject({ mode: "command", text: SELECTION });
|
|
119
|
+
});
|
|
120
|
+
});
|