@vellumai/assistant 0.11.8 → 0.11.9-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +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__/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__/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 +12 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +15 -1
- 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/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/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/injector.ts +14 -15
- 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 +12 -7
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +31 -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
|
@@ -686,28 +686,6 @@ describe("recordApprovalCardDelivery", () => {
|
|
|
686
686
|
expect(delivery?.destinationChatId).toBeNull();
|
|
687
687
|
expect(delivery?.status).toBe("pending");
|
|
688
688
|
});
|
|
689
|
-
|
|
690
|
-
test("lets a Slack reaction resolve back to its request (LUM-2502)", async () => {
|
|
691
|
-
// A delivered Slack approval card must be addressable by (channel, chat, ts)
|
|
692
|
-
// so an emoji reaction on it resolves to the right request rather than
|
|
693
|
-
// silently falling through to transcript persistence.
|
|
694
|
-
const req = makeRequest();
|
|
695
|
-
await recordApprovalCardDelivery({
|
|
696
|
-
requestId: req.id,
|
|
697
|
-
channel: "slack",
|
|
698
|
-
chatId: "C-guardian",
|
|
699
|
-
messageId: "1700000000.5678",
|
|
700
|
-
status: "sent",
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
const resolved =
|
|
704
|
-
await bridgeState.module.getPendingRequestByDestinationMessageOrNull(
|
|
705
|
-
"slack",
|
|
706
|
-
"C-guardian",
|
|
707
|
-
"1700000000.5678",
|
|
708
|
-
);
|
|
709
|
-
expect(resolved?.id).toBe(req.id);
|
|
710
|
-
});
|
|
711
689
|
});
|
|
712
690
|
|
|
713
691
|
describe("recordGuardianRequestDeliveries", () => {
|
|
@@ -454,25 +454,6 @@ export function createGuardianGatewaySim() {
|
|
|
454
454
|
.map((d) => ({ ...d }));
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
async function getPendingRequestByDestinationMessage(
|
|
458
|
-
channel: string,
|
|
459
|
-
chatId: string,
|
|
460
|
-
messageId: string,
|
|
461
|
-
): Promise<SimGuardianRequest | null> {
|
|
462
|
-
throwIfReadError();
|
|
463
|
-
const delivery = deliveries.find(
|
|
464
|
-
(d) =>
|
|
465
|
-
d.destinationChannel === channel &&
|
|
466
|
-
d.destinationChatId === chatId &&
|
|
467
|
-
d.destinationMessageId === messageId,
|
|
468
|
-
);
|
|
469
|
-
if (!delivery) {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
const request = requests.get(delivery.requestId);
|
|
473
|
-
return request?.status === "pending" ? { ...request } : null;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
457
|
async function listPendingRequestsByDestination(params: {
|
|
477
458
|
channel?: string;
|
|
478
459
|
chatId?: string;
|
|
@@ -624,10 +605,6 @@ export function createGuardianGatewaySim() {
|
|
|
624
605
|
listGuardianRequestDeliveries,
|
|
625
606
|
[],
|
|
626
607
|
),
|
|
627
|
-
getPendingRequestByDestinationMessageOrNull: degrade(
|
|
628
|
-
getPendingRequestByDestinationMessage,
|
|
629
|
-
null,
|
|
630
|
-
),
|
|
631
608
|
listPendingRequestsByDestinationOrEmpty: degrade(
|
|
632
609
|
listPendingRequestsByDestination,
|
|
633
610
|
[],
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
applyGuardianReplyMechanics,
|
|
4
5
|
buildGuardianCodeOnlyClarification,
|
|
5
6
|
buildGuardianDisambiguationExample,
|
|
6
7
|
buildGuardianDisambiguationLabel,
|
|
7
8
|
buildGuardianInvalidActionReply,
|
|
8
9
|
buildGuardianReplyDirective,
|
|
9
10
|
buildGuardianRequestCodeInstruction,
|
|
11
|
+
guardianCopyCarriesReplyMechanics,
|
|
10
12
|
hasGuardianRequestCodeInstruction,
|
|
11
13
|
parseGuardianQuestionPayload,
|
|
12
14
|
parseInteractiveApprovalPayload,
|
|
@@ -286,6 +288,112 @@ describe("guardian-question-mode", () => {
|
|
|
286
288
|
).not.toBeNull();
|
|
287
289
|
});
|
|
288
290
|
|
|
291
|
+
test("guardianCopyCarriesReplyMechanics: chat channels carry them, card surfaces do not", () => {
|
|
292
|
+
const approval = {
|
|
293
|
+
requestId: "req-1",
|
|
294
|
+
requestCode: "A1B2C3",
|
|
295
|
+
requestKind: "tool_grant_request",
|
|
296
|
+
questionText: "Allow bash?",
|
|
297
|
+
toolName: "bash",
|
|
298
|
+
};
|
|
299
|
+
const question = {
|
|
300
|
+
requestId: "req-2",
|
|
301
|
+
requestCode: "A1B2C3",
|
|
302
|
+
requestKind: "pending_question",
|
|
303
|
+
questionText: "Which one?",
|
|
304
|
+
};
|
|
305
|
+
// The bell, the banner, and the push act through the card whatever the mode.
|
|
306
|
+
expect(guardianCopyCarriesReplyMechanics("vellum", approval)).toBe(false);
|
|
307
|
+
expect(guardianCopyCarriesReplyMechanics("vellum", question)).toBe(false);
|
|
308
|
+
expect(guardianCopyCarriesReplyMechanics("platform", question)).toBe(false);
|
|
309
|
+
// Slack draws buttons for approvals only; a question is answered by text.
|
|
310
|
+
expect(guardianCopyCarriesReplyMechanics("slack", approval)).toBe(false);
|
|
311
|
+
expect(guardianCopyCarriesReplyMechanics("slack", question)).toBe(true);
|
|
312
|
+
expect(guardianCopyCarriesReplyMechanics("telegram", approval)).toBe(true);
|
|
313
|
+
expect(guardianCopyCarriesReplyMechanics("telegram", question)).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("applyGuardianReplyMechanics enforces into chat copy, strips from card-surface copy, and is idempotent", () => {
|
|
317
|
+
const signal = {
|
|
318
|
+
sourceEventName: "guardian.question",
|
|
319
|
+
contextPayload: {
|
|
320
|
+
requestId: "req-2",
|
|
321
|
+
requestCode: "a1b2c3",
|
|
322
|
+
requestKind: "pending_question",
|
|
323
|
+
questionText: "Which one?",
|
|
324
|
+
},
|
|
325
|
+
} as const;
|
|
326
|
+
const copy = {
|
|
327
|
+
title: "Question",
|
|
328
|
+
body: "Which one?\n\n1. Left\n2. Right",
|
|
329
|
+
conversationSeedMessage: "Which one?\n\n1. Left\n2. Right",
|
|
330
|
+
};
|
|
331
|
+
const instruction = 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".';
|
|
332
|
+
|
|
333
|
+
const telegram = applyGuardianReplyMechanics(copy, "telegram", signal);
|
|
334
|
+
expect(telegram.body).toBe(`${copy.body}\n\n${instruction}`);
|
|
335
|
+
expect(telegram.conversationSeedMessage).toBe(
|
|
336
|
+
`${copy.body}\n\n${instruction}`,
|
|
337
|
+
);
|
|
338
|
+
expect(applyGuardianReplyMechanics(telegram, "telegram", signal)).toEqual(
|
|
339
|
+
telegram,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const vellum = applyGuardianReplyMechanics(telegram, "vellum", signal);
|
|
343
|
+
expect(vellum).toEqual(copy);
|
|
344
|
+
expect(applyGuardianReplyMechanics(copy, "vellum", signal)).toEqual(copy);
|
|
345
|
+
|
|
346
|
+
// Any other signal passes through untouched.
|
|
347
|
+
expect(
|
|
348
|
+
applyGuardianReplyMechanics(copy, "telegram", {
|
|
349
|
+
...signal,
|
|
350
|
+
sourceEventName: "schedule.notify",
|
|
351
|
+
}),
|
|
352
|
+
).toEqual(copy);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("applyGuardianReplyMechanics replaces an instruction-only field with the question text on a card surface", () => {
|
|
356
|
+
const signal = {
|
|
357
|
+
sourceEventName: "guardian.question",
|
|
358
|
+
contextPayload: {
|
|
359
|
+
requestId: "req-3",
|
|
360
|
+
requestCode: "A1B2C3",
|
|
361
|
+
requestKind: "tool_grant_request",
|
|
362
|
+
questionText: "Allow bash to run ls /tmp?",
|
|
363
|
+
toolName: "bash",
|
|
364
|
+
},
|
|
365
|
+
} as const;
|
|
366
|
+
const instructionOnly = {
|
|
367
|
+
title: "Tool Grant Request. Approval code: A1B2C3",
|
|
368
|
+
body: 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
369
|
+
deliveryText: "Approval code: A1B2C3",
|
|
370
|
+
};
|
|
371
|
+
const vellum = applyGuardianReplyMechanics(
|
|
372
|
+
instructionOnly,
|
|
373
|
+
"vellum",
|
|
374
|
+
signal,
|
|
375
|
+
);
|
|
376
|
+
expect(vellum.title).toBe("Tool Grant Request.");
|
|
377
|
+
expect(vellum.body).toBe("Allow bash to run ls /tmp?");
|
|
378
|
+
expect(vellum.deliveryText).toBe("Allow bash to run ls /tmp?");
|
|
379
|
+
// A title is a headline, so a code-only one keeps its text.
|
|
380
|
+
expect(
|
|
381
|
+
applyGuardianReplyMechanics(
|
|
382
|
+
{ ...instructionOnly, title: "Approval code: A1B2C3" },
|
|
383
|
+
"vellum",
|
|
384
|
+
signal,
|
|
385
|
+
).title,
|
|
386
|
+
).toBe("Approval code: A1B2C3");
|
|
387
|
+
|
|
388
|
+
// With no question text to fall back on, the field keeps its text rather
|
|
389
|
+
// than becoming empty copy the broadcaster would skip.
|
|
390
|
+
const bare = applyGuardianReplyMechanics(instructionOnly, "vellum", {
|
|
391
|
+
...signal,
|
|
392
|
+
contextPayload: { ...signal.contextPayload, questionText: "" },
|
|
393
|
+
});
|
|
394
|
+
expect(bare.body).toBe(instructionOnly.body);
|
|
395
|
+
});
|
|
396
|
+
|
|
289
397
|
test("parseInteractiveApprovalPayload rejects answer-mode and unparseable payloads", () => {
|
|
290
398
|
// Answer mode: free-text questions get no Approve/Reject buttons.
|
|
291
399
|
expect(
|
|
@@ -44,7 +44,6 @@ mock.module("../channels/gateway-guardian-requests.js", () => ({
|
|
|
44
44
|
getGuardianRequestOrNull: (id: string) =>
|
|
45
45
|
Promise.resolve(requestsById.get(id) ?? null),
|
|
46
46
|
getGuardianRequestByCodeOrNull: () => Promise.resolve(null),
|
|
47
|
-
getPendingRequestByDestinationMessageOrNull: () => Promise.resolve(null),
|
|
48
47
|
listGuardianRequestsOrEmpty: () => Promise.resolve(pendingList),
|
|
49
48
|
}));
|
|
50
49
|
|
|
@@ -91,7 +90,6 @@ function makeRequest(
|
|
|
91
90
|
function makeContext(overrides: Record<string, unknown> = {}) {
|
|
92
91
|
return {
|
|
93
92
|
messageText: "",
|
|
94
|
-
channel: "telegram",
|
|
95
93
|
actor: {
|
|
96
94
|
actorPrincipalId: "prin-guardian",
|
|
97
95
|
actorExternalUserId: "tg-guardian",
|
|
@@ -106,7 +106,6 @@ function replyCtx(
|
|
|
106
106
|
): GuardianReplyContext {
|
|
107
107
|
return {
|
|
108
108
|
messageText: "",
|
|
109
|
-
channel: "telegram",
|
|
110
109
|
actor: guardianActor(),
|
|
111
110
|
conversationId: "conv-test",
|
|
112
111
|
...overrides,
|
|
@@ -1509,7 +1508,6 @@ describe("routing invariant: destination hints do not bypass tool_approval princ
|
|
|
1509
1508
|
const result = await routeGuardianReply(
|
|
1510
1509
|
replyCtx({
|
|
1511
1510
|
messageText: "approve",
|
|
1512
|
-
channel: "telegram",
|
|
1513
1511
|
actor: guardianActor({ guardianPrincipalId: "different-principal" }),
|
|
1514
1512
|
conversationId: "conv-guardian-chat",
|
|
1515
1513
|
pendingScope: { mode: "scoped", requestIds: [req.id] },
|
|
@@ -1544,7 +1542,6 @@ describe("routing invariant: destination hints do not bypass tool_approval princ
|
|
|
1544
1542
|
const result = await routeGuardianReply(
|
|
1545
1543
|
replyCtx({
|
|
1546
1544
|
messageText: "approve",
|
|
1547
|
-
channel: "telegram",
|
|
1548
1545
|
actor: guardianActor({ actorExternalUserId: "guardian-tg-user" }),
|
|
1549
1546
|
conversationId: "conv-guardian-chat",
|
|
1550
1547
|
// pendingScope omitted — no delivery hints
|
|
@@ -1704,7 +1701,6 @@ describe("routing invariant: invite handoff bypass for access requests", () => {
|
|
|
1704
1701
|
|
|
1705
1702
|
const result = await routeGuardianReply({
|
|
1706
1703
|
messageText: "C0D3A5 approve",
|
|
1707
|
-
channel: "vellum",
|
|
1708
1704
|
actor: trustedActor({ channel: "vellum" }),
|
|
1709
1705
|
conversationId: "conv-guardian-conversation",
|
|
1710
1706
|
pendingScope: { mode: "scoped", requestIds: [req.id] },
|
|
@@ -1743,7 +1739,6 @@ describe("routing invariant: invite handoff bypass for access requests", () => {
|
|
|
1743
1739
|
|
|
1744
1740
|
const result = await routeGuardianReply({
|
|
1745
1741
|
messageText: "please approve this request",
|
|
1746
|
-
channel: "vellum",
|
|
1747
1742
|
actor: trustedActor({ channel: "vellum" }),
|
|
1748
1743
|
conversationId: "conv-guardian-conversation",
|
|
1749
1744
|
pendingScope: { mode: "scoped", requestIds: [req.id] },
|
|
@@ -1907,16 +1902,35 @@ describe("routing invariant: kind-specific action sets in prompt mapping", () =>
|
|
|
1907
1902
|
expect(actions[1].action).toBe("reject");
|
|
1908
1903
|
});
|
|
1909
1904
|
|
|
1910
|
-
test("source-code invariant: guardian-action-routes.ts contains kind guard", () => {
|
|
1911
|
-
const srcRoot = resolve(__dirname, "..");
|
|
1912
|
-
const fullPath = join(srcRoot, "runtime/routes/guardian-action-routes.ts");
|
|
1913
|
-
const source = readFileSync(fullPath, "utf-8");
|
|
1914
|
-
expect(source).toContain('req.kind === "access_request"');
|
|
1915
|
-
});
|
|
1916
|
-
|
|
1917
1905
|
// Integration tests: verify listGuardianDecisionPrompts returns correct
|
|
1918
1906
|
// action sets for each guardian request kind.
|
|
1919
1907
|
|
|
1908
|
+
test("access_request prompt text carries the invite-flow sentence and no decision directive", async () => {
|
|
1909
|
+
const convId = "conv-kind-access-request";
|
|
1910
|
+
sim.seedRequest({
|
|
1911
|
+
kind: "access_request",
|
|
1912
|
+
sourceType: "channel",
|
|
1913
|
+
sourceConversationId: convId,
|
|
1914
|
+
guardianExternalUserId: "guardian-1",
|
|
1915
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1916
|
+
toolName: "ingress_access_request",
|
|
1917
|
+
questionText: "Alice is requesting access to the assistant.",
|
|
1918
|
+
requestCode: "A1B2C3",
|
|
1919
|
+
expiresAt: Date.now() + 60_000,
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
const prompts = await listGuardianDecisionPrompts({
|
|
1923
|
+
conversationId: convId,
|
|
1924
|
+
});
|
|
1925
|
+
expect(prompts).toHaveLength(1);
|
|
1926
|
+
// The actions carry the decision; the invite flow has no action anywhere,
|
|
1927
|
+
// so its sentence rides in the text.
|
|
1928
|
+
expect(prompts[0]!.questionText).toBe(
|
|
1929
|
+
'Alice is requesting access to the assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
|
|
1930
|
+
);
|
|
1931
|
+
expect(prompts[0]!.questionText).not.toContain("A1B2C3");
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1920
1934
|
test("tool_approval prompt uses approve_once + reject only (one-time decision pattern)", async () => {
|
|
1921
1935
|
const convId = "conv-kind-tool-approval";
|
|
1922
1936
|
sim.seedRequest({
|
|
@@ -17,6 +17,7 @@ describe("supportsHostProxy (runtime)", () => {
|
|
|
17
17
|
test("no-arg form returns true for host-proxy interfaces", () => {
|
|
18
18
|
expect(supportsHostProxy("macos")).toBe(true);
|
|
19
19
|
expect(supportsHostProxy("windows")).toBe(true);
|
|
20
|
+
expect(supportsHostProxy("linux")).toBe(true);
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
test("no-arg form returns false for interfaces without host-proxy support", () => {
|
|
@@ -59,6 +60,15 @@ describe("supportsHostProxy (runtime)", () => {
|
|
|
59
60
|
expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
test("capability form withholds unimplemented Linux app control", () => {
|
|
64
|
+
expect(supportsHostProxy("linux", "host_bash")).toBe(true);
|
|
65
|
+
expect(supportsHostProxy("linux", "host_file")).toBe(true);
|
|
66
|
+
expect(supportsHostProxy("linux", "host_cu")).toBe(true);
|
|
67
|
+
expect(supportsHostProxy("linux", "host_browser")).toBe(true);
|
|
68
|
+
expect(supportsHostProxy("linux", "host_ui_snapshot")).toBe(true);
|
|
69
|
+
expect(supportsHostProxy("linux", "host_app_control")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
62
72
|
test("capability form rejects everything for non-host-proxy interfaces", () => {
|
|
63
73
|
expect(supportsHostProxy("ios", "host_bash")).toBe(false);
|
|
64
74
|
expect(supportsHostProxy("cli", "host_file")).toBe(false);
|
|
@@ -103,6 +103,7 @@ function createTestContext(
|
|
|
103
103
|
: null;
|
|
104
104
|
|
|
105
105
|
let processing = false;
|
|
106
|
+
let owner = 0;
|
|
106
107
|
return {
|
|
107
108
|
conversationId: "conv-test",
|
|
108
109
|
messages: [],
|
|
@@ -110,6 +111,21 @@ function createTestContext(
|
|
|
110
111
|
setProcessing: (value: boolean) => {
|
|
111
112
|
processing = value;
|
|
112
113
|
},
|
|
114
|
+
acquireProcessingFenced: async () => {
|
|
115
|
+
if (processing) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
processing = true;
|
|
119
|
+
owner += 1;
|
|
120
|
+
return owner;
|
|
121
|
+
},
|
|
122
|
+
releaseProcessing: (claim: number) => {
|
|
123
|
+
if (claim !== owner) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
processing = false;
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
113
129
|
abortController: null,
|
|
114
130
|
queue: queueStub,
|
|
115
131
|
getTurnChannelContext: () => turnChannel,
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* pre-cutover v2 blocks both ride the cached prefix); the old whole-layer
|
|
11
11
|
* strip is gone.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* block
|
|
13
|
+
* Leftover `<memory_spotlight>` blocks are stripped from the TAIL user
|
|
14
|
+
* message only (mid-turn re-entry / post-compact must not double-stack).
|
|
15
|
+
* Historical user messages keep the spotlight they were sent with. The
|
|
16
|
+
* spotlight injector's `after-memory-prefix` block is spliced onto the
|
|
17
|
+
* current tail and captured on `blocks`.
|
|
17
18
|
*
|
|
18
19
|
* v2 suppression stays keyed off whether v3 produced a block, NOT off the
|
|
19
20
|
* gate alone: a v3 failure (`produce()` → null) leaves v2's block intact
|
|
@@ -128,7 +129,7 @@ function v3Injector(inner: string | null, commit?: () => void): Injector {
|
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
/** A fake v3 spotlight injector mirroring the real id +
|
|
132
|
+
/** A fake v3 spotlight injector mirroring the real id + placement. */
|
|
132
133
|
function spotlightInjector(inner: string): Injector {
|
|
133
134
|
return {
|
|
134
135
|
name: "memory-v3-spotlight",
|
|
@@ -137,7 +138,7 @@ function spotlightInjector(inner: string): Injector {
|
|
|
137
138
|
return {
|
|
138
139
|
id: "memory-v3-spotlight",
|
|
139
140
|
text: wrapMemorySpotlightBlock(inner),
|
|
140
|
-
placement: "
|
|
141
|
+
placement: "after-memory-prefix",
|
|
141
142
|
};
|
|
142
143
|
},
|
|
143
144
|
};
|
|
@@ -242,7 +243,7 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
242
243
|
expect(result.blocks.memoryV3Active).toBe(true);
|
|
243
244
|
});
|
|
244
245
|
|
|
245
|
-
test("
|
|
246
|
+
test("historical leftover spotlight stays; the fresh spotlight splices onto the tail", async () => {
|
|
246
247
|
memoryV3LiveSlot = true;
|
|
247
248
|
injectorChainSlot.push(v3Injector(""), spotlightInjector("fresh sections"));
|
|
248
249
|
|
|
@@ -266,16 +267,49 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
266
267
|
...makeTurnContext(),
|
|
267
268
|
});
|
|
268
269
|
|
|
269
|
-
//
|
|
270
|
+
// Historical turn keeps the spotlight it was sent with.
|
|
270
271
|
expect(result.messages[0].content).toEqual([
|
|
271
272
|
{ type: "text", text: "earlier question" },
|
|
273
|
+
staleSpotlight,
|
|
272
274
|
]);
|
|
273
|
-
//
|
|
275
|
+
// Fresh spotlight splices onto the tail (no memory prefix, so index 0)
|
|
276
|
+
// and is captured for metadata persist.
|
|
274
277
|
const texts = tailTexts(result.messages);
|
|
275
278
|
expect(texts).toEqual([
|
|
279
|
+
wrapMemorySpotlightBlock("fresh sections"),
|
|
276
280
|
"current question",
|
|
281
|
+
]);
|
|
282
|
+
expect(result.blocks.memoryV3SpotlightBlock).toBe(
|
|
277
283
|
wrapMemorySpotlightBlock("fresh sections"),
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("tail leftover spotlight is stripped before the fresh spotlight splices", async () => {
|
|
288
|
+
memoryV3LiveSlot = true;
|
|
289
|
+
injectorChainSlot.push(v3Injector(""), spotlightInjector("fresh sections"));
|
|
290
|
+
|
|
291
|
+
const staleOnTail = wrapMemorySpotlightBlock("stale sections");
|
|
292
|
+
const runMessages: Message[] = [
|
|
293
|
+
{
|
|
294
|
+
role: "user",
|
|
295
|
+
content: [
|
|
296
|
+
{ type: "text", text: staleOnTail },
|
|
297
|
+
{ type: "text", text: "current question" },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const result = await applyRuntimeInjections(runMessages, {
|
|
303
|
+
...makeTurnContext(),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(tailTexts(result.messages)).toEqual([
|
|
307
|
+
wrapMemorySpotlightBlock("fresh sections"),
|
|
308
|
+
"current question",
|
|
278
309
|
]);
|
|
310
|
+
expect(result.blocks.memoryV3SpotlightBlock).toBe(
|
|
311
|
+
wrapMemorySpotlightBlock("fresh sections"),
|
|
312
|
+
);
|
|
279
313
|
});
|
|
280
314
|
|
|
281
315
|
test("convergence re-entry: a tail leading with this turn's frozen v3 cards (and <info>) is NOT stripped", async () => {
|
|
@@ -491,6 +491,133 @@ describe("handleListMessages page=latest", () => {
|
|
|
491
491
|
});
|
|
492
492
|
});
|
|
493
493
|
|
|
494
|
+
test("an assistant row still carrying the partial pre-send envelope projects no slackMessage", async () => {
|
|
495
|
+
// The row an outbound Slack reply is reserved with names no id of its own:
|
|
496
|
+
// `channelTs` is back-filled by the post-send reconciliation. Until that
|
|
497
|
+
// lands, the strict reader rejects the envelope and the row projects
|
|
498
|
+
// nothing, so a client cannot attribute it to Slack. This is the silent
|
|
499
|
+
// half of the reconciliation gap, pinned so a reader that starts
|
|
500
|
+
// tolerating the partial form has to justify it here.
|
|
501
|
+
const conv = createConversation();
|
|
502
|
+
const db = getDb();
|
|
503
|
+
db.insert(messages)
|
|
504
|
+
.values({
|
|
505
|
+
id: "msg-slack-partial",
|
|
506
|
+
conversationId: conv.id,
|
|
507
|
+
role: "assistant",
|
|
508
|
+
content: JSON.stringify([{ type: "text", text: "Posted to Slack" }]),
|
|
509
|
+
metadata: JSON.stringify({
|
|
510
|
+
userMessageChannel: "slack",
|
|
511
|
+
assistantMessageChannel: "slack",
|
|
512
|
+
slackMeta: JSON.stringify({
|
|
513
|
+
source: "slack",
|
|
514
|
+
eventKind: "message",
|
|
515
|
+
channelId: "C123ABCDEF",
|
|
516
|
+
threadTs: "1710000000.000100",
|
|
517
|
+
timestampTimezone: "America/New_York",
|
|
518
|
+
timestampTimezoneLabel: "ET",
|
|
519
|
+
}),
|
|
520
|
+
}),
|
|
521
|
+
createdAt: 1,
|
|
522
|
+
})
|
|
523
|
+
.run();
|
|
524
|
+
|
|
525
|
+
const body = await callList({ conversationId: conv.id, page: "latest" });
|
|
526
|
+
|
|
527
|
+
expect(body.messages[0].slackMessage).toBeUndefined();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("a reconciled assistant row on the neutral envelope projects the same slackMessage", async () => {
|
|
531
|
+
// The row a Slack reply is written with today: the neutral envelope,
|
|
532
|
+
// Slack's own fields on its passthrough, `messageId` back-filled by the
|
|
533
|
+
// post-send reconciliation. The wire projection reads it through the
|
|
534
|
+
// envelope's Slack view, so the client sees exactly what a legacy
|
|
535
|
+
// `slackMeta` row projects.
|
|
536
|
+
const conv = createConversation();
|
|
537
|
+
const db = getDb();
|
|
538
|
+
db.insert(messages)
|
|
539
|
+
.values({
|
|
540
|
+
id: "msg-slack-neutral",
|
|
541
|
+
conversationId: conv.id,
|
|
542
|
+
role: "assistant",
|
|
543
|
+
content: JSON.stringify([{ type: "text", text: "Posted to Slack" }]),
|
|
544
|
+
metadata: JSON.stringify({
|
|
545
|
+
userMessageChannel: "slack",
|
|
546
|
+
assistantMessageChannel: "slack",
|
|
547
|
+
providerMeta: JSON.stringify({
|
|
548
|
+
source: "slack",
|
|
549
|
+
conversationExternalId: "C123ABCDEF",
|
|
550
|
+
messageId: "1710000000.000200",
|
|
551
|
+
threadId: "1710000000.000100",
|
|
552
|
+
eventKind: "message",
|
|
553
|
+
timestampTimezone: "America/New_York",
|
|
554
|
+
timestampTimezoneLabel: "ET",
|
|
555
|
+
}),
|
|
556
|
+
}),
|
|
557
|
+
createdAt: 1,
|
|
558
|
+
})
|
|
559
|
+
.run();
|
|
560
|
+
|
|
561
|
+
const body = await callList({ conversationId: conv.id, page: "latest" });
|
|
562
|
+
|
|
563
|
+
const slackMessage = body.messages[0].slackMessage;
|
|
564
|
+
expect(slackMessage).toBeDefined();
|
|
565
|
+
expect(slackMessage?.channelId).toBe("C123ABCDEF");
|
|
566
|
+
expect(slackMessage?.channelTs).toBe("1710000000.000200");
|
|
567
|
+
expect(slackMessage?.threadTs).toBe("1710000000.000100");
|
|
568
|
+
expect(slackMessage?.eventKind).toBe("message");
|
|
569
|
+
expect(slackMessage?.messageLink).toBeDefined();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("the reconciled assistant row projects slackMessage with the post's own ts", async () => {
|
|
573
|
+
// The other half of the same chain: once reconciliation stamps the ts the
|
|
574
|
+
// transport returned, the identical row projects a full envelope. Built by
|
|
575
|
+
// running the production reconciliation merge over the partial form rather
|
|
576
|
+
// than hand-writing the result, so the two stay in step.
|
|
577
|
+
const conv = createConversation();
|
|
578
|
+
const partial = {
|
|
579
|
+
source: "slack",
|
|
580
|
+
eventKind: "message" as const,
|
|
581
|
+
channelId: "C123ABCDEF",
|
|
582
|
+
threadTs: "1710000000.000100",
|
|
583
|
+
timestampTimezone: "America/New_York",
|
|
584
|
+
timestampTimezoneLabel: "ET",
|
|
585
|
+
};
|
|
586
|
+
const reconciled = JSON.stringify({
|
|
587
|
+
...partial,
|
|
588
|
+
channelTs: "1710000000.000200",
|
|
589
|
+
source: "slack",
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const db = getDb();
|
|
593
|
+
db.insert(messages)
|
|
594
|
+
.values({
|
|
595
|
+
id: "msg-slack-reconciled",
|
|
596
|
+
conversationId: conv.id,
|
|
597
|
+
role: "assistant",
|
|
598
|
+
content: JSON.stringify([{ type: "text", text: "Posted to Slack" }]),
|
|
599
|
+
metadata: JSON.stringify({
|
|
600
|
+
userMessageChannel: "slack",
|
|
601
|
+
assistantMessageChannel: "slack",
|
|
602
|
+
slackMeta: reconciled,
|
|
603
|
+
}),
|
|
604
|
+
createdAt: 1,
|
|
605
|
+
})
|
|
606
|
+
.run();
|
|
607
|
+
|
|
608
|
+
const body = await callList({ conversationId: conv.id, page: "latest" });
|
|
609
|
+
|
|
610
|
+
const slackMessage = body.messages[0].slackMessage;
|
|
611
|
+
expect(slackMessage).toBeDefined();
|
|
612
|
+
expect(slackMessage?.channelId).toBe("C123ABCDEF");
|
|
613
|
+
expect(slackMessage?.channelTs).toBe("1710000000.000200");
|
|
614
|
+
expect(slackMessage?.threadTs).toBe("1710000000.000100");
|
|
615
|
+
expect(slackMessage?.eventKind).toBe("message");
|
|
616
|
+
// The deep link is what the drawer's "open the source" action needs, and
|
|
617
|
+
// it is derivable only from the reconciled ts.
|
|
618
|
+
expect(slackMessage?.messageLink).toBeDefined();
|
|
619
|
+
});
|
|
620
|
+
|
|
494
621
|
test("top-level Slack messages with matching threadTs use plain permalinks", async () => {
|
|
495
622
|
const conv = createConversation();
|
|
496
623
|
const db = getDb();
|