@vellumai/assistant 0.8.3 → 0.8.4
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/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -12,6 +12,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
12
12
|
return {
|
|
13
13
|
...real,
|
|
14
14
|
memory: { ...real.memory, v2: { ...real.memory.v2, enabled: false } },
|
|
15
|
+
slack: { ...real.slack, botUserId: "U_BOT" },
|
|
15
16
|
};
|
|
16
17
|
},
|
|
17
18
|
}));
|
|
@@ -245,7 +246,7 @@ describe("injectChannelCapabilityContext", () => {
|
|
|
245
246
|
expect(text).toContain("CHANNEL CONSTRAINTS");
|
|
246
247
|
expect(text).toContain("Do NOT reference the dashboard UI");
|
|
247
248
|
expect(text).toContain("Do NOT use ui_show");
|
|
248
|
-
expect(text).toContain("
|
|
249
|
+
expect(text).not.toContain("microphone");
|
|
249
250
|
expect(text).toContain("dashboard_capable: false");
|
|
250
251
|
});
|
|
251
252
|
|
|
@@ -337,6 +338,36 @@ describe("injectChannelCapabilityContext", () => {
|
|
|
337
338
|
expect(text).toContain("emoji reactions");
|
|
338
339
|
});
|
|
339
340
|
|
|
341
|
+
test("allows only task_progress ui_show/ui_update guidance for Slack", () => {
|
|
342
|
+
const caps: ChannelCapabilities = {
|
|
343
|
+
channel: "slack",
|
|
344
|
+
dashboardCapable: false,
|
|
345
|
+
supportsDynamicUi: false,
|
|
346
|
+
supportsVoiceInput: false,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const result = injectChannelCapabilityContext(baseUserMessage, caps);
|
|
350
|
+
const text = (result.content[0] as { type: "text"; text: string }).text;
|
|
351
|
+
expect(text).toContain(
|
|
352
|
+
'Only use ui_show/ui_update for card surfaces with template: "task_progress"',
|
|
353
|
+
);
|
|
354
|
+
expect(text).not.toContain("Do NOT use ui_show, ui_update, or app_create");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("keeps blanket ui_show/ui_update prohibition for other non-dynamic channels", () => {
|
|
358
|
+
const caps: ChannelCapabilities = {
|
|
359
|
+
channel: "phone",
|
|
360
|
+
dashboardCapable: false,
|
|
361
|
+
supportsDynamicUi: false,
|
|
362
|
+
supportsVoiceInput: false,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const result = injectChannelCapabilityContext(baseUserMessage, caps);
|
|
366
|
+
const text = (result.content[0] as { type: "text"; text: string }).text;
|
|
367
|
+
expect(text).toContain("Do NOT use ui_show, ui_update, or app_create");
|
|
368
|
+
expect(text).not.toContain("Only use ui_show/ui_update");
|
|
369
|
+
});
|
|
370
|
+
|
|
340
371
|
test("still injects for group chats even when all capabilities are true", () => {
|
|
341
372
|
const caps: ChannelCapabilities = {
|
|
342
373
|
channel: "slack",
|
|
@@ -611,7 +642,7 @@ describe("trust-gating via channel capabilities", () => {
|
|
|
611
642
|
});
|
|
612
643
|
|
|
613
644
|
test("non-dashboard channel adds constraint rules preventing UI references", () => {
|
|
614
|
-
const caps = resolveChannelCapabilities("
|
|
645
|
+
const caps = resolveChannelCapabilities("telegram");
|
|
615
646
|
const message: Message = {
|
|
616
647
|
role: "user",
|
|
617
648
|
content: [{ type: "text", text: "Show me a chart" }],
|
|
@@ -624,7 +655,8 @@ describe("trust-gating via channel capabilities", () => {
|
|
|
624
655
|
expect(injected).toContain("Do NOT reference the dashboard UI");
|
|
625
656
|
expect(injected).toContain("Do NOT use ui_show, ui_update, or app_create");
|
|
626
657
|
expect(injected).toContain("Present information as well-formatted text");
|
|
627
|
-
expect(injected).toContain("
|
|
658
|
+
expect(injected).not.toContain("accent color selection");
|
|
659
|
+
expect(injected).not.toContain("complete those steps");
|
|
628
660
|
});
|
|
629
661
|
|
|
630
662
|
test("vellum web interface allows dynamic UI but constrains dashboard references", () => {
|
|
@@ -1250,6 +1282,24 @@ describe("buildUnifiedTurnContextBlock", () => {
|
|
|
1250
1282
|
expect(telegramText).toContain("<no_response/>");
|
|
1251
1283
|
});
|
|
1252
1284
|
|
|
1285
|
+
test("adds task_progress hint only for Slack turns", () => {
|
|
1286
|
+
const slackText = buildUnifiedTurnContextBlock({
|
|
1287
|
+
timestamp: "2026-04-02T12:00:00Z",
|
|
1288
|
+
interfaceName: "slack",
|
|
1289
|
+
channelName: "slack",
|
|
1290
|
+
});
|
|
1291
|
+
const telegramText = buildUnifiedTurnContextBlock({
|
|
1292
|
+
timestamp: "2026-04-02T12:00:00Z",
|
|
1293
|
+
interfaceName: "telegram",
|
|
1294
|
+
channelName: "telegram",
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
expect(slackText).toContain(
|
|
1298
|
+
"if you are going to do work, use task_progress",
|
|
1299
|
+
);
|
|
1300
|
+
expect(telegramText).not.toContain("use task_progress");
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1253
1303
|
test("dedup logic: fields matching canonical_actor_identity are omitted", () => {
|
|
1254
1304
|
const uuid = "vellum-principal-b77e94f5-67c0-4599-8baa-871b925b3da8";
|
|
1255
1305
|
const options: UnifiedTurnContextOptions = {
|
|
@@ -2295,7 +2345,6 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2295
2345
|
const T1 = "1700000010.000002"; // top-level message starting thread B
|
|
2296
2346
|
const T2 = "1700000030.000003"; // newer top-level message
|
|
2297
2347
|
const ALIAS_T0 = parentAlias(T0);
|
|
2298
|
-
const ALIAS_T1 = parentAlias(T1);
|
|
2299
2348
|
const ALIAS_T2 = parentAlias(T2);
|
|
2300
2349
|
|
|
2301
2350
|
const SLACK_CHANNEL_ID = "C0123CHANNEL";
|
|
@@ -2490,16 +2539,16 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2490
2539
|
expect(lines[2]).toContain("Reply in thread B");
|
|
2491
2540
|
expect(lines[3]).toContain("Reply in thread A");
|
|
2492
2541
|
// Cross-thread visibility: thread B's reply is in the rendered output
|
|
2493
|
-
// alongside thread A's reply.
|
|
2494
|
-
expect(lines[2]).toContain(
|
|
2495
|
-
expect(lines[3]).toContain(
|
|
2542
|
+
// alongside thread A's reply, without parent-arrow prefixes.
|
|
2543
|
+
expect(lines[2]).not.toContain("→ M");
|
|
2544
|
+
expect(lines[3]).not.toContain("→ M");
|
|
2496
2545
|
// Sender labels appear.
|
|
2497
2546
|
expect(lines[0]).toContain("alice");
|
|
2498
2547
|
expect(lines[1]).toContain("bob");
|
|
2499
2548
|
});
|
|
2500
2549
|
|
|
2501
2550
|
// ── Scenario 2: reply to a top-level (starts new thread) ─────────────
|
|
2502
|
-
test("scenario 2 — reply to top-level renders
|
|
2551
|
+
test("scenario 2 — reply to top-level renders without parent arrow", async () => {
|
|
2503
2552
|
const rows: MessageRow[] = [
|
|
2504
2553
|
userRow({
|
|
2505
2554
|
id: "m1",
|
|
@@ -2523,15 +2572,13 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2523
2572
|
const lines = texts(result);
|
|
2524
2573
|
|
|
2525
2574
|
expect(lines.length).toBe(2);
|
|
2526
|
-
// Top-level has no thread tag.
|
|
2527
2575
|
expect(lines[0]).not.toContain("→ M");
|
|
2528
|
-
|
|
2529
|
-
expect(lines[1]).toContain(`→ ${ALIAS_T0}`);
|
|
2576
|
+
expect(lines[1]).not.toContain("→ M");
|
|
2530
2577
|
expect(lines[1]).toContain("Reply that starts a new thread");
|
|
2531
2578
|
});
|
|
2532
2579
|
|
|
2533
2580
|
// ── Scenario 3: reply to the most-recent top-level message ───────────
|
|
2534
|
-
test("scenario 3 — reply to last top-level still renders
|
|
2581
|
+
test("scenario 3 — reply to last top-level still renders chronologically", async () => {
|
|
2535
2582
|
const rows: MessageRow[] = [
|
|
2536
2583
|
userRow({
|
|
2537
2584
|
id: "m1",
|
|
@@ -2561,13 +2608,12 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2561
2608
|
const lines = texts(result);
|
|
2562
2609
|
|
|
2563
2610
|
expect(lines.length).toBe(3);
|
|
2564
|
-
|
|
2565
|
-
expect(lines[2]).toContain(
|
|
2566
|
-
expect(lines[2]).not.toContain(`→ ${ALIAS_T0}`);
|
|
2611
|
+
expect(lines[2]).toContain("Reply to the newer top-level");
|
|
2612
|
+
expect(lines[2]).not.toContain("→ M");
|
|
2567
2613
|
});
|
|
2568
2614
|
|
|
2569
2615
|
// ── Scenario 4: brand-new top-level message ──────────────────────────
|
|
2570
|
-
test("scenario 4 — new top-level message has no
|
|
2616
|
+
test("scenario 4 — new top-level message has no parent arrow", async () => {
|
|
2571
2617
|
const rows: MessageRow[] = [
|
|
2572
2618
|
userRow({
|
|
2573
2619
|
id: "m1",
|
|
@@ -2587,7 +2633,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2587
2633
|
const lines = texts(result);
|
|
2588
2634
|
|
|
2589
2635
|
expect(lines.length).toBe(2);
|
|
2590
|
-
// Both lines render without a
|
|
2636
|
+
// Both lines render without a parent arrow — they are siblings, not
|
|
2591
2637
|
// members of the same thread.
|
|
2592
2638
|
expect(lines[0]).not.toContain("→ M");
|
|
2593
2639
|
expect(lines[1]).not.toContain("→ M");
|
|
@@ -2602,9 +2648,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2602
2648
|
// ── Scenario 5: legacy mixed with post-upgrade rows ──────────────────
|
|
2603
2649
|
// Pre-upgrade rows have no `slackMeta` sub-key. Post-upgrade rows have
|
|
2604
2650
|
// it. Both kinds must appear in the rendered transcript with legacy
|
|
2605
|
-
// rows
|
|
2606
|
-
//
|
|
2607
|
-
//
|
|
2651
|
+
// rows and post-upgrade rows both rendered without parent-arrow prefixes.
|
|
2652
|
+
// The renderer's chronological sort must intermix them on the appropriate
|
|
2653
|
+
// timeline.
|
|
2608
2654
|
test("scenario 5 — legacy rows mixed with post-upgrade rows render chronologically", async () => {
|
|
2609
2655
|
const rows: MessageRow[] = [
|
|
2610
2656
|
// Legacy user row with a displayName hint only — no slackMeta.
|
|
@@ -2621,8 +2667,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2621
2667
|
text: "Legacy assistant reply",
|
|
2622
2668
|
}),
|
|
2623
2669
|
// Post-upgrade row anchored to a thread parent that has no record
|
|
2624
|
-
// in storage (legacy parent)
|
|
2625
|
-
// because the metadata is intact.
|
|
2670
|
+
// in storage (legacy parent).
|
|
2626
2671
|
userRow({
|
|
2627
2672
|
id: "m3",
|
|
2628
2673
|
createdAt: 1700000000_000,
|
|
@@ -2645,11 +2690,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2645
2690
|
expect(lines[0]).toContain("Legacy user message");
|
|
2646
2691
|
expect(lines[1]).toContain("Legacy assistant reply");
|
|
2647
2692
|
expect(lines[2]).toContain("Post-upgrade thread reply");
|
|
2648
|
-
// Legacy rows render flat — no thread tag arrow.
|
|
2649
2693
|
expect(lines[0]).not.toContain("→ M");
|
|
2650
2694
|
expect(lines[1]).not.toContain("→ M");
|
|
2651
|
-
|
|
2652
|
-
expect(lines[2]).toContain(`→ ${ALIAS_T0}`);
|
|
2695
|
+
expect(lines[2]).not.toContain("→ M");
|
|
2653
2696
|
// Sender labels: legacy rows carry no structured displayName, and the
|
|
2654
2697
|
// role slot already conveys user-vs-assistant identity, so the row
|
|
2655
2698
|
// mapper emits `null` senderLabel and the renderer omits the label
|
|
@@ -2967,37 +3010,6 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2967
3010
|
expect(allText).not.toContain("dm context");
|
|
2968
3011
|
});
|
|
2969
3012
|
|
|
2970
|
-
test("slack late-join notice is model-facing and non-persisted", async () => {
|
|
2971
|
-
const slackChannelCaps: ChannelCapabilities = {
|
|
2972
|
-
channel: "slack",
|
|
2973
|
-
dashboardCapable: false,
|
|
2974
|
-
supportsDynamicUi: false,
|
|
2975
|
-
supportsVoiceInput: false,
|
|
2976
|
-
chatType: "channel",
|
|
2977
|
-
};
|
|
2978
|
-
const notice =
|
|
2979
|
-
"Slack context note: this turn joined an existing thread. 3 earlier thread messages were backfilled before the current message.";
|
|
2980
|
-
|
|
2981
|
-
const { messages: result, blocks } = await applyRuntimeInjections(
|
|
2982
|
-
[{ role: "user", content: [{ type: "text", text: "current turn" }] }],
|
|
2983
|
-
{
|
|
2984
|
-
channelCapabilities: slackChannelCaps,
|
|
2985
|
-
slackRuntimeContextNotice: notice,
|
|
2986
|
-
transportHints: [notice],
|
|
2987
|
-
},
|
|
2988
|
-
);
|
|
2989
|
-
|
|
2990
|
-
const allText = result
|
|
2991
|
-
.flatMap((m) => m.content)
|
|
2992
|
-
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
2993
|
-
.map((b) => b.text)
|
|
2994
|
-
.join("\n");
|
|
2995
|
-
expect(allText).toContain("<slack_context_notice>");
|
|
2996
|
-
expect(allText).toContain(notice);
|
|
2997
|
-
expect(allText).not.toContain("<transport_hints>");
|
|
2998
|
-
expect(JSON.stringify(blocks)).not.toContain(notice);
|
|
2999
|
-
});
|
|
3000
|
-
|
|
3001
3013
|
// ── transport_hints kept for non-slack channels ───────────────────────
|
|
3002
3014
|
test("non-slack conversations still receive <transport_hints>", async () => {
|
|
3003
3015
|
const { messages: result } = await applyRuntimeInjections(
|
|
@@ -3023,10 +3035,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3023
3035
|
});
|
|
3024
3036
|
|
|
3025
3037
|
// ── trust-filter regression for loadSlackChronologicalMessages ───────
|
|
3026
|
-
// For untrusted actors,
|
|
3027
|
-
//
|
|
3028
|
-
|
|
3029
|
-
test("loadSlackChronologicalMessages filters guardian-scoped rows for untrusted actors", () => {
|
|
3038
|
+
// For untrusted actors, Slack-sourced rows are still shared channel/thread
|
|
3039
|
+
// context, while non-Slack guardian-scoped rows remain private.
|
|
3040
|
+
test("loadSlackChronologicalMessages keeps Slack-visible guardian rows for untrusted actors", () => {
|
|
3030
3041
|
const caps: ChannelCapabilities = {
|
|
3031
3042
|
channel: "slack",
|
|
3032
3043
|
dashboardCapable: false,
|
|
@@ -3034,14 +3045,15 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3034
3045
|
supportsVoiceInput: false,
|
|
3035
3046
|
chatType: "channel",
|
|
3036
3047
|
};
|
|
3037
|
-
// Row 1 has no provenance → guardian-scoped (filtered out).
|
|
3038
|
-
// Row 2 has provenance.trustClass === "trusted_contact" (kept).
|
|
3039
3048
|
const rows: MessageRow[] = [
|
|
3040
3049
|
userRow({
|
|
3041
3050
|
id: "m1",
|
|
3042
3051
|
createdAt: 1700000000_000,
|
|
3043
|
-
text: "guardian
|
|
3052
|
+
text: "public guardian instruction",
|
|
3044
3053
|
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3054
|
+
extraOuterMetadata: {
|
|
3055
|
+
provenanceTrustClass: "guardian",
|
|
3056
|
+
},
|
|
3045
3057
|
}),
|
|
3046
3058
|
userRow({
|
|
3047
3059
|
id: "m2",
|
|
@@ -3052,6 +3064,14 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3052
3064
|
provenanceTrustClass: "trusted_contact",
|
|
3053
3065
|
},
|
|
3054
3066
|
}),
|
|
3067
|
+
userRow({
|
|
3068
|
+
id: "m3",
|
|
3069
|
+
createdAt: 1700000020_000,
|
|
3070
|
+
text: "private guardian-only context",
|
|
3071
|
+
extraOuterMetadata: {
|
|
3072
|
+
provenanceTrustClass: "guardian",
|
|
3073
|
+
},
|
|
3074
|
+
}),
|
|
3055
3075
|
];
|
|
3056
3076
|
const result = loadSlackChronologicalMessages("conv-1", caps, {
|
|
3057
3077
|
loader: () => rows,
|
|
@@ -3063,8 +3083,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3063
3083
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
3064
3084
|
.map((b) => b.text)
|
|
3065
3085
|
.join("\n");
|
|
3066
|
-
expect(allText).
|
|
3086
|
+
expect(allText).toContain("public guardian instruction");
|
|
3067
3087
|
expect(allText).toContain("from untrusted actor");
|
|
3088
|
+
expect(allText).not.toContain("private guardian-only context");
|
|
3068
3089
|
});
|
|
3069
3090
|
|
|
3070
3091
|
test("loadSlackChronologicalContext preserves summary and filters by Slack watermark", () => {
|
|
@@ -3382,15 +3403,14 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3382
3403
|
expect(focusBlock).not.toBeNull();
|
|
3383
3404
|
expect(focusBlock!).toContain("<active_thread>");
|
|
3384
3405
|
expect(focusBlock!).toContain("</active_thread>");
|
|
3385
|
-
// Parent (T0) is included
|
|
3406
|
+
// Parent (T0) is included by content.
|
|
3386
3407
|
expect(focusBlock!).toContain("Top-level in thread A");
|
|
3387
3408
|
// The new reply is included.
|
|
3388
3409
|
expect(focusBlock!).toContain("New reply in thread A");
|
|
3389
|
-
expect(focusBlock!).toContain(
|
|
3410
|
+
expect(focusBlock!).not.toContain("→ M");
|
|
3390
3411
|
// Thread B's content is NOT in the focus block.
|
|
3391
3412
|
expect(focusBlock!).not.toContain("Top-level in thread B");
|
|
3392
3413
|
expect(focusBlock!).not.toContain("Cross-thread reply in B");
|
|
3393
|
-
expect(focusBlock!).not.toContain(`→ ${ALIAS_T1}`);
|
|
3394
3414
|
|
|
3395
3415
|
// The focus block is appended to the FINAL user message as a tail
|
|
3396
3416
|
// text block — not to any earlier message.
|
|
@@ -3988,13 +4008,97 @@ describe("assembleSlackActiveThreadFocusBlock", () => {
|
|
|
3988
4008
|
expect(result!).toContain("@assistant: Assistant reply");
|
|
3989
4009
|
});
|
|
3990
4010
|
|
|
4011
|
+
test("timezone-aware assistant rows keep renderer attribution in active-thread focus block", () => {
|
|
4012
|
+
const rows: SlackTranscriptInputRow[] = [
|
|
4013
|
+
buildRow(
|
|
4014
|
+
"user",
|
|
4015
|
+
"Parent",
|
|
4016
|
+
1_000,
|
|
4017
|
+
buildMeta({
|
|
4018
|
+
channelTs: PARENT_TS,
|
|
4019
|
+
displayName: "aaron",
|
|
4020
|
+
timestampTimezone: "America/Denver",
|
|
4021
|
+
timestampTimezoneLabel: "MT",
|
|
4022
|
+
}),
|
|
4023
|
+
),
|
|
4024
|
+
buildRow(
|
|
4025
|
+
"assistant",
|
|
4026
|
+
"Assistant reply",
|
|
4027
|
+
2_000,
|
|
4028
|
+
buildMeta({
|
|
4029
|
+
channelTs: "1700000005.000001",
|
|
4030
|
+
threadTs: PARENT_TS,
|
|
4031
|
+
timestampTimezone: "America/Denver",
|
|
4032
|
+
timestampTimezoneLabel: "MT",
|
|
4033
|
+
speakerTimezoneLabel: "ET",
|
|
4034
|
+
}),
|
|
4035
|
+
),
|
|
4036
|
+
buildRow(
|
|
4037
|
+
"user",
|
|
4038
|
+
"Follow-up",
|
|
4039
|
+
3_000,
|
|
4040
|
+
buildMeta({
|
|
4041
|
+
channelTs: REPLY_TS,
|
|
4042
|
+
threadTs: PARENT_TS,
|
|
4043
|
+
displayName: "aaron",
|
|
4044
|
+
timestampTimezone: "America/Denver",
|
|
4045
|
+
timestampTimezoneLabel: "MT",
|
|
4046
|
+
}),
|
|
4047
|
+
),
|
|
4048
|
+
];
|
|
4049
|
+
|
|
4050
|
+
const result = assembleSlackActiveThreadFocusBlock(rows, SLACK_CAPS);
|
|
4051
|
+
expect(result).not.toBeNull();
|
|
4052
|
+
expect(result!).toContain(
|
|
4053
|
+
"[nov 14 2023 3:13 PM MT assistant] Assistant reply",
|
|
4054
|
+
);
|
|
4055
|
+
expect(result!).not.toContain(
|
|
4056
|
+
"@assistant: [nov 14 2023 3:13 PM MT assistant]",
|
|
4057
|
+
);
|
|
4058
|
+
});
|
|
4059
|
+
|
|
4060
|
+
test("assistant content that only looks like a compact tag still gets active-thread attribution", () => {
|
|
4061
|
+
const compactLookingContent =
|
|
4062
|
+
"[nov 14 2023 3:13 PM MT assistant] Assistant reply";
|
|
4063
|
+
const rows: SlackTranscriptInputRow[] = [
|
|
4064
|
+
buildRow(
|
|
4065
|
+
"user",
|
|
4066
|
+
"Parent",
|
|
4067
|
+
1_000,
|
|
4068
|
+
buildMeta({ channelTs: PARENT_TS, displayName: "@alice" }),
|
|
4069
|
+
),
|
|
4070
|
+
buildRow(
|
|
4071
|
+
"assistant",
|
|
4072
|
+
compactLookingContent,
|
|
4073
|
+
2_000,
|
|
4074
|
+
buildMeta({
|
|
4075
|
+
channelTs: "1700000005.000001",
|
|
4076
|
+
threadTs: PARENT_TS,
|
|
4077
|
+
}),
|
|
4078
|
+
),
|
|
4079
|
+
buildRow(
|
|
4080
|
+
"user",
|
|
4081
|
+
"Follow-up",
|
|
4082
|
+
3_000,
|
|
4083
|
+
buildMeta({
|
|
4084
|
+
channelTs: REPLY_TS,
|
|
4085
|
+
threadTs: PARENT_TS,
|
|
4086
|
+
displayName: "@alice",
|
|
4087
|
+
}),
|
|
4088
|
+
),
|
|
4089
|
+
];
|
|
4090
|
+
|
|
4091
|
+
const result = assembleSlackActiveThreadFocusBlock(rows, SLACK_CAPS);
|
|
4092
|
+
expect(result).not.toBeNull();
|
|
4093
|
+
expect(result!).toContain(`@assistant: ${compactLookingContent}`);
|
|
4094
|
+
});
|
|
4095
|
+
|
|
3991
4096
|
test("assistant reaction overflow trailer is not double-attributed", () => {
|
|
3992
4097
|
// When assistant reactions overflow the per-target cap, `renderSlackTranscript`
|
|
3993
4098
|
// emits a trailer line (`[…and N more reactions to Mxxxxxx]`) whose role
|
|
3994
|
-
// is inherited from the first overflowing reaction — i.e. `assistant`.
|
|
3995
|
-
//
|
|
3996
|
-
//
|
|
3997
|
-
// be detected by `isReactionTagLine` and skipped by the prefix step.
|
|
4099
|
+
// is inherited from the first overflowing reaction — i.e. `assistant`.
|
|
4100
|
+
// Renderer provenance marks it as a Slack reaction line so the flattened
|
|
4101
|
+
// active-thread block does not add a content-message prefix.
|
|
3998
4102
|
const PARENT_ALIAS_TS = PARENT_TS;
|
|
3999
4103
|
const buildAssistantReaction = (ts: string, emoji: string) =>
|
|
4000
4104
|
buildRow(
|
|
@@ -4079,6 +4183,7 @@ describe("assembleSlackChronologicalMessages", () => {
|
|
|
4079
4183
|
// Anchor times mirror the renderer's HH:MM (UTC) output.
|
|
4080
4184
|
// 14:25:00 UTC on 2023-11-14 = epoch second 1699971900.
|
|
4081
4185
|
const TS_14_25 = "1699971900.000100"; // 14:25 UTC
|
|
4186
|
+
const TS_14_26 = "1699971960.000200"; // 14:26 UTC
|
|
4082
4187
|
const TS_14_28 = "1699972080.000300"; // 14:28 UTC
|
|
4083
4188
|
const MS_14_25 = 1699971900_000;
|
|
4084
4189
|
const MS_14_26 = 1699971960_000;
|
|
@@ -4239,6 +4344,85 @@ describe("assembleSlackChronologicalMessages", () => {
|
|
|
4239
4344
|
}
|
|
4240
4345
|
});
|
|
4241
4346
|
|
|
4347
|
+
test("expanded Slack timezone metadata remains renderable", () => {
|
|
4348
|
+
const userMeta: SlackMessageMetadata = {
|
|
4349
|
+
source: "slack",
|
|
4350
|
+
channelId: DM_CHANNEL_ID,
|
|
4351
|
+
channelTs: TS_14_25,
|
|
4352
|
+
eventKind: "message",
|
|
4353
|
+
displayName: "@alice",
|
|
4354
|
+
actorTimezone: "America/New_York",
|
|
4355
|
+
actorTimezoneLabel: "ET",
|
|
4356
|
+
actorTimezoneOffsetSeconds: -18000,
|
|
4357
|
+
timestampTimezone: "America/New_York",
|
|
4358
|
+
timestampTimezoneLabel: "ET",
|
|
4359
|
+
speakerTimezoneLabel: "ET",
|
|
4360
|
+
};
|
|
4361
|
+
const rows: SlackTranscriptInputRow[] = [
|
|
4362
|
+
row("user", "timezone-aware hello", MS_14_25, metadataEnvelope(userMeta)),
|
|
4363
|
+
];
|
|
4364
|
+
|
|
4365
|
+
const result = assembleSlackChronologicalMessages(rows, DM_CAPS);
|
|
4366
|
+
expect(result).not.toBeNull();
|
|
4367
|
+
expect(result!.map((m) => (m.content[0] as { text: string }).text)).toEqual(
|
|
4368
|
+
[
|
|
4369
|
+
`[nov 14 2023 9:25 AM ET @alice (ET)] ${slackExternal(
|
|
4370
|
+
"timezone-aware hello",
|
|
4371
|
+
"@alice",
|
|
4372
|
+
)}`,
|
|
4373
|
+
],
|
|
4374
|
+
);
|
|
4375
|
+
});
|
|
4376
|
+
|
|
4377
|
+
test("Slack context skips configured assistant new-thread placeholder rows", () => {
|
|
4378
|
+
const placeholderMeta: SlackMessageMetadata = {
|
|
4379
|
+
source: "slack",
|
|
4380
|
+
channelId: DM_CHANNEL_ID,
|
|
4381
|
+
channelTs: TS_14_25,
|
|
4382
|
+
eventKind: "message",
|
|
4383
|
+
displayName: "Ada",
|
|
4384
|
+
actorExternalUserId: "U_BOT",
|
|
4385
|
+
};
|
|
4386
|
+
const otherBotMeta: SlackMessageMetadata = {
|
|
4387
|
+
source: "slack",
|
|
4388
|
+
channelId: DM_CHANNEL_ID,
|
|
4389
|
+
channelTs: TS_14_26,
|
|
4390
|
+
eventKind: "message",
|
|
4391
|
+
displayName: "Build Bot",
|
|
4392
|
+
actorExternalUserId: "B_OTHER",
|
|
4393
|
+
};
|
|
4394
|
+
const realBotMeta: SlackMessageMetadata = {
|
|
4395
|
+
source: "slack",
|
|
4396
|
+
channelId: DM_CHANNEL_ID,
|
|
4397
|
+
channelTs: TS_14_28,
|
|
4398
|
+
eventKind: "message",
|
|
4399
|
+
actorExternalUserId: "B_ASSISTANT",
|
|
4400
|
+
};
|
|
4401
|
+
const rows: SlackTranscriptInputRow[] = [
|
|
4402
|
+
row(
|
|
4403
|
+
"user",
|
|
4404
|
+
"New Assistant Thread",
|
|
4405
|
+
MS_14_25,
|
|
4406
|
+
metadataEnvelope(placeholderMeta),
|
|
4407
|
+
),
|
|
4408
|
+
row(
|
|
4409
|
+
"user",
|
|
4410
|
+
"New Assistant Thread",
|
|
4411
|
+
MS_14_26,
|
|
4412
|
+
metadataEnvelope(otherBotMeta),
|
|
4413
|
+
),
|
|
4414
|
+
row("user", "real bot context", MS_14_28, metadataEnvelope(realBotMeta)),
|
|
4415
|
+
];
|
|
4416
|
+
|
|
4417
|
+
const result = assembleSlackChronologicalMessages(rows, DM_CAPS);
|
|
4418
|
+
expect(result).not.toBeNull();
|
|
4419
|
+
const rendered = JSON.stringify(result);
|
|
4420
|
+
expect(rendered).not.toContain("Ada");
|
|
4421
|
+
expect(rendered.split("New Assistant Thread").length - 1).toBe(1);
|
|
4422
|
+
expect(rendered).toContain("Build Bot");
|
|
4423
|
+
expect(rendered).toContain("real bot context");
|
|
4424
|
+
});
|
|
4425
|
+
|
|
4242
4426
|
test("legacy-DM fixture: pre-upgrade rows (no slackMeta) interleave with post-upgrade rows", () => {
|
|
4243
4427
|
// Mix:
|
|
4244
4428
|
// - Two pre-upgrade rows (created before PR 16 wired slackMeta into
|
|
@@ -4452,17 +4636,16 @@ describe("assembleSlackChronologicalMessages", () => {
|
|
|
4452
4636
|
});
|
|
4453
4637
|
});
|
|
4454
4638
|
|
|
4455
|
-
test("post-reconciliation: assistant rows with channelTs participate in
|
|
4639
|
+
test("post-reconciliation: assistant rows with channelTs participate in chronological rendering", () => {
|
|
4456
4640
|
// Once `deliverReplyViaCallback` reconciles `channelTs` from the
|
|
4457
4641
|
// gateway's response, assistant rows carry a fully-formed slackMeta
|
|
4458
4642
|
// envelope. They must then render through the Slack chronological
|
|
4459
4643
|
// path (not the legacy fallback) so reply rows pointing at the
|
|
4460
|
-
// assistant's prior message
|
|
4644
|
+
// assistant's prior message appear in Slack timestamp order.
|
|
4461
4645
|
//
|
|
4462
4646
|
// This is the cross-thread visibility that the slack-thread-aware-
|
|
4463
4647
|
// context plan promises: a follow-up user reply to the assistant's
|
|
4464
|
-
// earlier post should render
|
|
4465
|
-
// can use to reason about which prior assistant message it threads off.
|
|
4648
|
+
// earlier post should render alongside the prior assistant row.
|
|
4466
4649
|
const SLACK_CHANNEL_ID_2 = "C0THREAD";
|
|
4467
4650
|
const ASSISTANT_TS = "1700001000.000111";
|
|
4468
4651
|
const REPLY_TS = "1700001020.000222";
|
|
@@ -4507,13 +4690,13 @@ describe("assembleSlackChronologicalMessages", () => {
|
|
|
4507
4690
|
expect(result).not.toBeNull();
|
|
4508
4691
|
expect(result!.length).toBe(2);
|
|
4509
4692
|
|
|
4510
|
-
// The user follow-up
|
|
4511
|
-
//
|
|
4512
|
-
// assistant row was treated as legacy/null-metadata and excluded from
|
|
4513
|
-
// alias issuance — the user reply rendered without the arrow.
|
|
4693
|
+
// The user follow-up keeps timestamp/sender attribution without carrying
|
|
4694
|
+
// the old parent-alias arrow.
|
|
4514
4695
|
const replyText = (result![1].content[0] as { text: string }).text;
|
|
4515
|
-
expect(replyText).
|
|
4516
|
-
|
|
4696
|
+
expect(replyText).toBe(
|
|
4697
|
+
`[11/14/23 22:30 @alice]: ${slackExternal("Following up", "@alice")}`,
|
|
4698
|
+
);
|
|
4699
|
+
expect(replyText).not.toContain("→ M");
|
|
4517
4700
|
});
|
|
4518
4701
|
|
|
4519
4702
|
test("post-reconciliation: assistant row appears in active-thread focus block", () => {
|
|
@@ -175,7 +175,7 @@ describe("isConversationSeedSane", () => {
|
|
|
175
175
|
|
|
176
176
|
describe("composeConversationSeed", () => {
|
|
177
177
|
describe("rich verbosity (vellum/macos)", () => {
|
|
178
|
-
test("
|
|
178
|
+
test("omits title when conversationTitle is absent (it's the chat header)", () => {
|
|
179
179
|
const signal = makeSignal();
|
|
180
180
|
const copy = makeCopy({ title: "Reminder", body: "Take out the trash" });
|
|
181
181
|
const seed = composeConversationSeed(
|
|
@@ -183,12 +183,71 @@ describe("composeConversationSeed", () => {
|
|
|
183
183
|
"vellum" as NotificationChannel,
|
|
184
184
|
copy,
|
|
185
185
|
);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
// The conversation header already shows copy.title — including it in
|
|
187
|
+
// the bubble would duplicate it.
|
|
188
|
+
expect(seed).toBe("Take out the trash");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("includes title when conversationTitle provides a distinct header", () => {
|
|
192
|
+
const signal = makeSignal();
|
|
193
|
+
const copy = makeCopy({
|
|
194
|
+
conversationTitle: "Updates",
|
|
195
|
+
title: "Heart rate spike",
|
|
196
|
+
body: "You hit 103 bpm.",
|
|
197
|
+
});
|
|
198
|
+
const seed = composeConversationSeed(
|
|
199
|
+
signal,
|
|
200
|
+
"vellum" as NotificationChannel,
|
|
201
|
+
copy,
|
|
202
|
+
);
|
|
203
|
+
expect(seed).toContain("Heart rate spike");
|
|
204
|
+
expect(seed).toContain("You hit 103 bpm");
|
|
189
205
|
expect(seed).not.toContain("\n");
|
|
190
206
|
});
|
|
191
207
|
|
|
208
|
+
test("does not duplicate title when body already starts with it", () => {
|
|
209
|
+
const signal = makeSignal();
|
|
210
|
+
const copy = makeCopy({
|
|
211
|
+
title: "Status update — service running",
|
|
212
|
+
body: "Status update — service running since 12:00 PM.",
|
|
213
|
+
});
|
|
214
|
+
const seed = composeConversationSeed(
|
|
215
|
+
signal,
|
|
216
|
+
"vellum" as NotificationChannel,
|
|
217
|
+
copy,
|
|
218
|
+
);
|
|
219
|
+
// Without the fix, this would produce
|
|
220
|
+
// "Status update — service running. Status update — service running since 12:00 PM."
|
|
221
|
+
expect(seed).toBe("Status update — service running since 12:00 PM.");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("falls back to title when body is empty and title is the header", () => {
|
|
225
|
+
const signal = makeSignal();
|
|
226
|
+
const copy = makeCopy({ title: "Reminder", body: "" });
|
|
227
|
+
const seed = composeConversationSeed(
|
|
228
|
+
signal,
|
|
229
|
+
"vellum" as NotificationChannel,
|
|
230
|
+
copy,
|
|
231
|
+
);
|
|
232
|
+
// Even though the title is the conversation header, keeping the
|
|
233
|
+
// bubble non-empty wins over avoiding the redundancy.
|
|
234
|
+
expect(seed).toBe("Reminder");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("falls back to title when body is whitespace-only", () => {
|
|
238
|
+
const signal = makeSignal();
|
|
239
|
+
const copy = makeCopy({ title: "Reminder", body: " " });
|
|
240
|
+
const seed = composeConversationSeed(
|
|
241
|
+
signal,
|
|
242
|
+
"vellum" as NotificationChannel,
|
|
243
|
+
copy,
|
|
244
|
+
);
|
|
245
|
+
// Whitespace-only bodies must be treated as empty — otherwise the
|
|
246
|
+
// title would be suppressed (header dedupe) and the seed would
|
|
247
|
+
// render as a blank bubble.
|
|
248
|
+
expect(seed).toBe("Reminder");
|
|
249
|
+
});
|
|
250
|
+
|
|
192
251
|
test('appends "Action required." when requiresAction is true', () => {
|
|
193
252
|
const signal = makeSignal({
|
|
194
253
|
attentionHints: {
|
|
@@ -277,6 +336,7 @@ describe("composeConversationSeed", () => {
|
|
|
277
336
|
test("preserves localized LLM copy on vellum (rich)", () => {
|
|
278
337
|
const signal = makeSignal();
|
|
279
338
|
const copy = makeCopy({
|
|
339
|
+
conversationTitle: "通知",
|
|
280
340
|
title: "リマインダー",
|
|
281
341
|
body: "ゴミを出してください",
|
|
282
342
|
});
|
|
@@ -285,6 +345,7 @@ describe("composeConversationSeed", () => {
|
|
|
285
345
|
"vellum" as NotificationChannel,
|
|
286
346
|
copy,
|
|
287
347
|
);
|
|
348
|
+
// conversationTitle is set, so the rich seed includes both title and body.
|
|
288
349
|
expect(seed).toContain("リマインダー");
|
|
289
350
|
expect(seed).toContain("ゴミを出してください");
|
|
290
351
|
});
|
|
@@ -314,6 +375,7 @@ describe("composeConversationSeed", () => {
|
|
|
314
375
|
},
|
|
315
376
|
});
|
|
316
377
|
const copy = makeCopy({
|
|
378
|
+
conversationTitle: "ガーディアン",
|
|
317
379
|
title: "ガーディアンの質問",
|
|
318
380
|
body: "ゲートコードは何ですか?",
|
|
319
381
|
});
|