@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
|
@@ -77,6 +77,14 @@ mock.module("../messaging/providers/slack/adapter.js", () => ({
|
|
|
77
77
|
_account: string | undefined,
|
|
78
78
|
fn: (token: string) => Promise<unknown>,
|
|
79
79
|
) => fn("test-slack-token"),
|
|
80
|
+
resolveSlackBotUserId: async (
|
|
81
|
+
_account: string | undefined,
|
|
82
|
+
botId: string,
|
|
83
|
+
) => {
|
|
84
|
+
if (botId === "B_ASSISTANT") return "U_BOT";
|
|
85
|
+
if (botId === "B_OTHER") return "U_OTHER_BOT";
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
80
88
|
}));
|
|
81
89
|
|
|
82
90
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +93,11 @@ mock.module("../messaging/providers/slack/adapter.js", () => ({
|
|
|
85
93
|
|
|
86
94
|
import { v4 as uuid } from "uuid";
|
|
87
95
|
|
|
96
|
+
import {
|
|
97
|
+
loadRawConfig,
|
|
98
|
+
saveRawConfig,
|
|
99
|
+
setNestedValue,
|
|
100
|
+
} from "../config/loader.js";
|
|
88
101
|
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
89
102
|
import {
|
|
90
103
|
type ChannelCapabilities,
|
|
@@ -97,6 +110,7 @@ import { recordInbound } from "../memory/delivery-crud.js";
|
|
|
97
110
|
import type { Message as MessagingMessage } from "../messaging/provider-types.js";
|
|
98
111
|
import * as slackBackfill from "../messaging/providers/slack/backfill.js";
|
|
99
112
|
import {
|
|
113
|
+
buildSlackTimezoneMetadata,
|
|
100
114
|
readSlackMetadata,
|
|
101
115
|
writeSlackMetadata,
|
|
102
116
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
@@ -162,6 +176,20 @@ function resetState(): void {
|
|
|
162
176
|
backfillDmMock.mockImplementation(async () => []);
|
|
163
177
|
downloadSlackFileMock.mockReset();
|
|
164
178
|
downloadSlackFileMock.mockResolvedValue(null);
|
|
179
|
+
setConfiguredSlackBotUserId("U_BOT");
|
|
180
|
+
setConfiguredUserTimezone(undefined);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function setConfiguredSlackBotUserId(botUserId: string): void {
|
|
184
|
+
const raw = loadRawConfig();
|
|
185
|
+
setNestedValue(raw, "slack.botUserId", botUserId);
|
|
186
|
+
saveRawConfig(raw);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function setConfiguredUserTimezone(userTimezone: string | undefined): void {
|
|
190
|
+
const raw = loadRawConfig();
|
|
191
|
+
setNestedValue(raw, "ui.userTimezone", userTimezone);
|
|
192
|
+
saveRawConfig(raw);
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
let convCounter = 0;
|
|
@@ -269,6 +297,12 @@ interface PersistedRow {
|
|
|
269
297
|
threadTs: string | undefined;
|
|
270
298
|
displayName: string | undefined;
|
|
271
299
|
actorExternalUserId: string | undefined;
|
|
300
|
+
actorTimezone: string | undefined;
|
|
301
|
+
actorTimezoneLabel: string | undefined;
|
|
302
|
+
actorTimezoneOffsetSeconds: number | undefined;
|
|
303
|
+
timestampTimezone: string | undefined;
|
|
304
|
+
timestampTimezoneLabel: string | undefined;
|
|
305
|
+
speakerTimezoneLabel: string | undefined;
|
|
272
306
|
slackFiles: Array<{ name: string; mimetype?: string }> | undefined;
|
|
273
307
|
provenanceTrustClass: string | undefined;
|
|
274
308
|
provenanceSourceChannel: string | undefined;
|
|
@@ -296,6 +330,12 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
|
|
|
296
330
|
threadTs: undefined,
|
|
297
331
|
displayName: undefined,
|
|
298
332
|
actorExternalUserId: undefined,
|
|
333
|
+
actorTimezone: undefined,
|
|
334
|
+
actorTimezoneLabel: undefined,
|
|
335
|
+
actorTimezoneOffsetSeconds: undefined,
|
|
336
|
+
timestampTimezone: undefined,
|
|
337
|
+
timestampTimezoneLabel: undefined,
|
|
338
|
+
speakerTimezoneLabel: undefined,
|
|
299
339
|
slackFiles: undefined,
|
|
300
340
|
provenanceTrustClass: undefined,
|
|
301
341
|
provenanceSourceChannel: undefined,
|
|
@@ -336,6 +376,12 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
|
|
|
336
376
|
threadTs: slackMeta?.threadTs,
|
|
337
377
|
displayName: slackMeta?.displayName,
|
|
338
378
|
actorExternalUserId: slackMeta?.actorExternalUserId,
|
|
379
|
+
actorTimezone: slackMeta?.actorTimezone,
|
|
380
|
+
actorTimezoneLabel: slackMeta?.actorTimezoneLabel,
|
|
381
|
+
actorTimezoneOffsetSeconds: slackMeta?.actorTimezoneOffsetSeconds,
|
|
382
|
+
timestampTimezone: slackMeta?.timestampTimezone,
|
|
383
|
+
timestampTimezoneLabel: slackMeta?.timestampTimezoneLabel,
|
|
384
|
+
speakerTimezoneLabel: slackMeta?.speakerTimezoneLabel,
|
|
339
385
|
slackFiles: slackMeta?.slackFiles?.map((file) => ({
|
|
340
386
|
name: file.name,
|
|
341
387
|
...(file.mimetype ? { mimetype: file.mimetype } : {}),
|
|
@@ -978,7 +1024,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
978
1024
|
expect(contextImage).toBeDefined();
|
|
979
1025
|
});
|
|
980
1026
|
|
|
981
|
-
test("backfilled
|
|
1027
|
+
test("backfilled assistant Slack image files are persisted as assistant image history", async () => {
|
|
982
1028
|
const conv = createTestConversation();
|
|
983
1029
|
|
|
984
1030
|
seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
|
|
@@ -992,7 +1038,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
992
1038
|
id: "1234.1",
|
|
993
1039
|
text: "bot posted a diagram",
|
|
994
1040
|
threadId: "1234.0",
|
|
995
|
-
sender: { id: "
|
|
1041
|
+
sender: { id: "U_BOT", name: "Douglas" },
|
|
996
1042
|
metadata: {
|
|
997
1043
|
isBot: true,
|
|
998
1044
|
slackFiles: [
|
|
@@ -1020,7 +1066,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
1020
1066
|
row.content.includes("bot posted a diagram"),
|
|
1021
1067
|
);
|
|
1022
1068
|
expect(botRow).toBeDefined();
|
|
1023
|
-
expect(botRow?.role).toBe("
|
|
1069
|
+
expect(botRow?.role).toBe("assistant");
|
|
1024
1070
|
const blocks = JSON.parse(botRow!.content) as Message["content"];
|
|
1025
1071
|
const textBlock = blocks.find(
|
|
1026
1072
|
(block): block is Extract<Message["content"][number], { type: "text" }> =>
|
|
@@ -1042,6 +1088,27 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
1042
1088
|
});
|
|
1043
1089
|
|
|
1044
1090
|
expect(context).not.toBeNull();
|
|
1091
|
+
const contextBotMessage = context!.messages.find(
|
|
1092
|
+
(message) =>
|
|
1093
|
+
message.role === "assistant" &&
|
|
1094
|
+
message.content.some(
|
|
1095
|
+
(block) =>
|
|
1096
|
+
block.type === "text" &&
|
|
1097
|
+
block.text.includes("bot posted a diagram"),
|
|
1098
|
+
),
|
|
1099
|
+
);
|
|
1100
|
+
expect(contextBotMessage).toBeDefined();
|
|
1101
|
+
expect(
|
|
1102
|
+
contextBotMessage!.content
|
|
1103
|
+
.filter(
|
|
1104
|
+
(
|
|
1105
|
+
block,
|
|
1106
|
+
): block is Extract<Message["content"][number], { type: "text" }> =>
|
|
1107
|
+
block.type === "text",
|
|
1108
|
+
)
|
|
1109
|
+
.map((block) => block.text)
|
|
1110
|
+
.join("\n"),
|
|
1111
|
+
).not.toContain("<external_content");
|
|
1045
1112
|
const contextImage = context!.messages
|
|
1046
1113
|
.flatMap((message) => message.content)
|
|
1047
1114
|
.find((block) => block.type === "image");
|
|
@@ -1082,6 +1149,62 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
1082
1149
|
expect(persisted.provenanceRequesterIdentifier).toBe("U_ANITA");
|
|
1083
1150
|
});
|
|
1084
1151
|
|
|
1152
|
+
test("backfilled Slack timezone metadata derives timestamp and speaker fields", async () => {
|
|
1153
|
+
const conv = createTestConversation();
|
|
1154
|
+
setConfiguredUserTimezone("America/Denver");
|
|
1155
|
+
|
|
1156
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
1157
|
+
makeBackfillMessage({
|
|
1158
|
+
id: "1234.0",
|
|
1159
|
+
text: "non-guardian context",
|
|
1160
|
+
threadId: undefined,
|
|
1161
|
+
sender: { id: "U_ANITA", name: "Anita" },
|
|
1162
|
+
metadata: {
|
|
1163
|
+
actorTimezone: "America/New_York",
|
|
1164
|
+
actorTimezoneLabel: "ET",
|
|
1165
|
+
actorTimezoneOffsetSeconds: -18000,
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
makeBackfillMessage({
|
|
1169
|
+
id: "1234.1",
|
|
1170
|
+
text: "trusted context",
|
|
1171
|
+
threadId: "1234.0",
|
|
1172
|
+
sender: { id: "U_GUARDIAN", name: "Guardian User" },
|
|
1173
|
+
metadata: {
|
|
1174
|
+
actorTimezone: "America/Denver",
|
|
1175
|
+
actorTimezoneLabel: "MT",
|
|
1176
|
+
actorTimezoneOffsetSeconds: -25200,
|
|
1177
|
+
},
|
|
1178
|
+
}),
|
|
1179
|
+
]);
|
|
1180
|
+
|
|
1181
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
1182
|
+
conversationId: conv.id,
|
|
1183
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1184
|
+
threadTs: "1234.0",
|
|
1185
|
+
guardianExternalUserId: "U_GUARDIAN",
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const persisted = readPersistedSlackRows(conv.id);
|
|
1189
|
+
const nonGuardian = persisted.find((p) => p.channelTs === "1234.0");
|
|
1190
|
+
expect(nonGuardian).toBeDefined();
|
|
1191
|
+
expect(nonGuardian?.actorTimezone).toBe("America/New_York");
|
|
1192
|
+
expect(nonGuardian?.actorTimezoneLabel).toBe("ET");
|
|
1193
|
+
expect(nonGuardian?.actorTimezoneOffsetSeconds).toBe(-18000);
|
|
1194
|
+
expect(nonGuardian?.timestampTimezone).toBe("America/Denver");
|
|
1195
|
+
expect(nonGuardian?.timestampTimezoneLabel).toBe("MT");
|
|
1196
|
+
expect(nonGuardian?.speakerTimezoneLabel).toBe("ET");
|
|
1197
|
+
|
|
1198
|
+
const guardian = persisted.find((p) => p.channelTs === "1234.1");
|
|
1199
|
+
expect(guardian).toBeDefined();
|
|
1200
|
+
expect(guardian?.actorTimezone).toBe("America/Denver");
|
|
1201
|
+
expect(guardian?.actorTimezoneLabel).toBe("MT");
|
|
1202
|
+
expect(guardian?.actorTimezoneOffsetSeconds).toBe(-25200);
|
|
1203
|
+
expect(guardian?.timestampTimezone).toBe("America/Denver");
|
|
1204
|
+
expect(guardian?.timestampTimezoneLabel).toBe("MT");
|
|
1205
|
+
expect(guardian?.speakerTimezoneLabel).toBeUndefined();
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1085
1208
|
test("backfilled guardian-authored text is persisted raw with guardian provenance", async () => {
|
|
1086
1209
|
const conv = createTestConversation();
|
|
1087
1210
|
|
|
@@ -1115,12 +1238,13 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
1115
1238
|
expect(persisted.provenanceRequesterIdentifier).toBe("U_GUARDIAN");
|
|
1116
1239
|
});
|
|
1117
1240
|
|
|
1118
|
-
test("backfilled
|
|
1241
|
+
test("backfilled assistant-authored text is persisted raw as assistant history", async () => {
|
|
1119
1242
|
const conv = createTestConversation();
|
|
1243
|
+
setConfiguredUserTimezone("America/Denver");
|
|
1120
1244
|
|
|
1121
1245
|
backfillThreadMock.mockImplementation(async () => [
|
|
1122
1246
|
makeBackfillMessage({
|
|
1123
|
-
id: "
|
|
1247
|
+
id: "1772681880.000000",
|
|
1124
1248
|
text: "earlier assistant reply",
|
|
1125
1249
|
threadId: undefined,
|
|
1126
1250
|
sender: { id: "U_BOT", name: "Douglas" },
|
|
@@ -1131,20 +1255,150 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
|
|
|
1131
1255
|
await triggerSlackThreadBackfillIfNeeded({
|
|
1132
1256
|
conversationId: conv.id,
|
|
1133
1257
|
channelId: SLACK_CHANNEL_ID,
|
|
1134
|
-
threadTs: "
|
|
1258
|
+
threadTs: "1772681880.000000",
|
|
1135
1259
|
});
|
|
1136
1260
|
|
|
1137
1261
|
const [persisted] = readPersistedSlackRows(conv.id).filter(
|
|
1138
|
-
(p) => p.channelTs === "
|
|
1262
|
+
(p) => p.channelTs === "1772681880.000000",
|
|
1139
1263
|
);
|
|
1140
1264
|
expect(persisted).toBeDefined();
|
|
1141
|
-
expect(persisted.role).toBe("
|
|
1265
|
+
expect(persisted.role).toBe("assistant");
|
|
1142
1266
|
expect(persisted.rawContent).toBe("earlier assistant reply");
|
|
1143
1267
|
expect(persisted.rawContent).not.toContain("<external_content");
|
|
1144
1268
|
expect(persisted.actorExternalUserId).toBe("U_BOT");
|
|
1269
|
+
expect(persisted.timestampTimezone).toBe("America/Denver");
|
|
1270
|
+
expect(persisted.timestampTimezoneLabel).toBe("MT");
|
|
1271
|
+
expect(persisted.speakerTimezoneLabel).toBeUndefined();
|
|
1145
1272
|
expect(persisted.provenanceTrustClass).toBe("unknown");
|
|
1146
1273
|
expect(persisted.provenanceSourceChannel).toBe("slack");
|
|
1147
1274
|
expect(persisted.provenanceRequesterIdentifier).toBe("U_BOT");
|
|
1275
|
+
|
|
1276
|
+
const context = loadSlackChronologicalContext(conv.id, SLACK_CHANNEL_CAPS, {
|
|
1277
|
+
loader: readMessageRowsByConversation,
|
|
1278
|
+
trustClass: "guardian",
|
|
1279
|
+
});
|
|
1280
|
+
expect(context).not.toBeNull();
|
|
1281
|
+
expect(context!.messages).toEqual([
|
|
1282
|
+
{
|
|
1283
|
+
role: "assistant",
|
|
1284
|
+
content: [
|
|
1285
|
+
{
|
|
1286
|
+
type: "text",
|
|
1287
|
+
text: "[mar 4 2026 8:38 PM MT assistant] earlier assistant reply",
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
},
|
|
1291
|
+
]);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
test("backfilled bot-id-only assistant text resolves to assistant history", async () => {
|
|
1295
|
+
const conv = createTestConversation();
|
|
1296
|
+
|
|
1297
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
1298
|
+
makeBackfillMessage({
|
|
1299
|
+
id: "1234.0",
|
|
1300
|
+
text: "earlier assistant reply from bot id",
|
|
1301
|
+
threadId: undefined,
|
|
1302
|
+
sender: { id: "B_ASSISTANT", name: "Douglas" },
|
|
1303
|
+
metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
|
|
1304
|
+
}),
|
|
1305
|
+
]);
|
|
1306
|
+
|
|
1307
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
1308
|
+
conversationId: conv.id,
|
|
1309
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1310
|
+
threadTs: "1234.0",
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
const [persisted] = readPersistedSlackRows(conv.id).filter(
|
|
1314
|
+
(p) => p.channelTs === "1234.0",
|
|
1315
|
+
);
|
|
1316
|
+
expect(persisted).toBeDefined();
|
|
1317
|
+
expect(persisted.role).toBe("assistant");
|
|
1318
|
+
expect(persisted.rawContent).toBe("earlier assistant reply from bot id");
|
|
1319
|
+
expect(persisted.actorExternalUserId).toBe("B_ASSISTANT");
|
|
1320
|
+
expect(persisted.provenanceRequesterIdentifier).toBe("B_ASSISTANT");
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
test("backfilled third-party bot-authored text stays user history", async () => {
|
|
1324
|
+
const conv = createTestConversation();
|
|
1325
|
+
|
|
1326
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
1327
|
+
makeBackfillMessage({
|
|
1328
|
+
id: "1234.0",
|
|
1329
|
+
text: "deployment bot status update",
|
|
1330
|
+
threadId: undefined,
|
|
1331
|
+
sender: { id: "U_OTHER_BOT", name: "Deploy Bot" },
|
|
1332
|
+
metadata: { isBot: true, slackBotId: "B_OTHER" },
|
|
1333
|
+
}),
|
|
1334
|
+
]);
|
|
1335
|
+
|
|
1336
|
+
await triggerSlackThreadBackfillIfNeeded({
|
|
1337
|
+
conversationId: conv.id,
|
|
1338
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1339
|
+
threadTs: "1234.0",
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
const [persisted] = readPersistedSlackRows(conv.id).filter(
|
|
1343
|
+
(p) => p.channelTs === "1234.0",
|
|
1344
|
+
);
|
|
1345
|
+
expect(persisted).toBeDefined();
|
|
1346
|
+
expect(persisted.role).toBe("user");
|
|
1347
|
+
expect(persisted.rawContent).toBe("deployment bot status update");
|
|
1348
|
+
expect(persisted.actorExternalUserId).toBe("U_OTHER_BOT");
|
|
1349
|
+
expect(persisted.provenanceTrustClass).toBe("unknown");
|
|
1350
|
+
expect(persisted.provenanceSourceChannel).toBe("slack");
|
|
1351
|
+
expect(persisted.provenanceRequesterIdentifier).toBe("U_OTHER_BOT");
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
test("skips Slack assistant new-thread placeholder during backfill", async () => {
|
|
1355
|
+
const conv = createTestConversation();
|
|
1356
|
+
|
|
1357
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
1358
|
+
makeBackfillMessage({
|
|
1359
|
+
id: "1234.0",
|
|
1360
|
+
text: "New Assistant Thread",
|
|
1361
|
+
threadId: undefined,
|
|
1362
|
+
sender: { id: "B_ASSISTANT", name: "Ada" },
|
|
1363
|
+
metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
|
|
1364
|
+
}),
|
|
1365
|
+
makeBackfillMessage({
|
|
1366
|
+
id: "1234.1",
|
|
1367
|
+
text: "real bot context",
|
|
1368
|
+
threadId: "1234.0",
|
|
1369
|
+
sender: { id: "B_ASSISTANT", name: "Ada" },
|
|
1370
|
+
metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
|
|
1371
|
+
}),
|
|
1372
|
+
makeBackfillMessage({
|
|
1373
|
+
id: "1234.2",
|
|
1374
|
+
text: "New Assistant Thread",
|
|
1375
|
+
threadId: undefined,
|
|
1376
|
+
sender: { id: "B_OTHER", name: "Build Bot" },
|
|
1377
|
+
metadata: { isBot: true, slackBotId: "B_OTHER" },
|
|
1378
|
+
}),
|
|
1379
|
+
]);
|
|
1380
|
+
|
|
1381
|
+
const result = await triggerSlackThreadBackfillIfNeeded({
|
|
1382
|
+
conversationId: conv.id,
|
|
1383
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1384
|
+
threadTs: "1234.0",
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
expect(result.fetched).toBe(3);
|
|
1388
|
+
expect(result.persisted).toBe(2);
|
|
1389
|
+
const rows = readPersistedSlackRows(conv.id);
|
|
1390
|
+
expect(rows.map((row) => row.rawContent).sort()).toEqual([
|
|
1391
|
+
"New Assistant Thread",
|
|
1392
|
+
"real bot context",
|
|
1393
|
+
]);
|
|
1394
|
+
const assistantRow = rows.find(
|
|
1395
|
+
(row) => row.rawContent === "real bot context",
|
|
1396
|
+
);
|
|
1397
|
+
expect(assistantRow?.role).toBe("assistant");
|
|
1398
|
+
expect(assistantRow?.actorExternalUserId).toBe("B_ASSISTANT");
|
|
1399
|
+
expect(rows.some((row) => row.actorExternalUserId === "B_OTHER")).toBe(
|
|
1400
|
+
true,
|
|
1401
|
+
);
|
|
1148
1402
|
});
|
|
1149
1403
|
|
|
1150
1404
|
test("backfilled non-bot message with empty text is persisted unwrapped", async () => {
|
|
@@ -1775,13 +2029,25 @@ function buildSlackDmRequest(
|
|
|
1775
2029
|
|
|
1776
2030
|
interface SlackInboundProcessOptions {
|
|
1777
2031
|
displayContent?: string;
|
|
1778
|
-
|
|
2032
|
+
transport?: {
|
|
2033
|
+
channelId?: string;
|
|
2034
|
+
hints?: string[];
|
|
2035
|
+
uxBrief?: string;
|
|
2036
|
+
chatType?: string;
|
|
2037
|
+
clientTimezone?: string;
|
|
2038
|
+
};
|
|
1779
2039
|
slackInbound?: {
|
|
1780
2040
|
channelId: string;
|
|
1781
2041
|
channelTs: string;
|
|
1782
2042
|
threadTs?: string;
|
|
1783
2043
|
displayName?: string;
|
|
1784
2044
|
actorExternalUserId?: string;
|
|
2045
|
+
actorTimezone?: string;
|
|
2046
|
+
actorTimezoneLabel?: string;
|
|
2047
|
+
actorTimezoneOffsetSeconds?: number;
|
|
2048
|
+
timestampTimezone?: string;
|
|
2049
|
+
timestampTimezoneLabel?: string;
|
|
2050
|
+
speakerTimezoneLabel?: string;
|
|
1785
2051
|
};
|
|
1786
2052
|
}
|
|
1787
2053
|
|
|
@@ -1808,6 +2074,15 @@ function persistSlackInboundFromProcessMessage(
|
|
|
1808
2074
|
...(slackInbound.actorExternalUserId
|
|
1809
2075
|
? { actorExternalUserId: slackInbound.actorExternalUserId }
|
|
1810
2076
|
: {}),
|
|
2077
|
+
...buildSlackTimezoneMetadata({
|
|
2078
|
+
actorTimezone: slackInbound.actorTimezone,
|
|
2079
|
+
actorTimezoneLabel: slackInbound.actorTimezoneLabel,
|
|
2080
|
+
actorTimezoneOffsetSeconds:
|
|
2081
|
+
slackInbound.actorTimezoneOffsetSeconds,
|
|
2082
|
+
timestampTimezone: slackInbound.timestampTimezone,
|
|
2083
|
+
timestampTimezoneLabel: slackInbound.timestampTimezoneLabel,
|
|
2084
|
+
speakerTimezoneLabel: slackInbound.speakerTimezoneLabel,
|
|
2085
|
+
}),
|
|
1811
2086
|
eventKind: "message",
|
|
1812
2087
|
}),
|
|
1813
2088
|
}
|
|
@@ -1934,18 +2209,15 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
1934
2209
|
]);
|
|
1935
2210
|
|
|
1936
2211
|
let capturedHints: string[] | undefined;
|
|
1937
|
-
let capturedSlackNotice: string | undefined;
|
|
1938
2212
|
const processMessage = async (
|
|
1939
2213
|
_conversationId: string,
|
|
1940
2214
|
_content: string,
|
|
1941
2215
|
_attachmentIds?: string[],
|
|
1942
2216
|
options?: {
|
|
1943
2217
|
transport?: { hints?: string[] };
|
|
1944
|
-
slackRuntimeContextNotice?: string;
|
|
1945
2218
|
},
|
|
1946
2219
|
): Promise<{ messageId: string }> => {
|
|
1947
2220
|
capturedHints = options?.transport?.hints;
|
|
1948
|
-
capturedSlackNotice = options?.slackRuntimeContextNotice;
|
|
1949
2221
|
return { messageId: "agent-result-id" };
|
|
1950
2222
|
};
|
|
1951
2223
|
setAdapterProcessMessage(processMessage);
|
|
@@ -1992,16 +2264,7 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
1992
2264
|
expect(channelTimestamps.has("1234.0")).toBe(true);
|
|
1993
2265
|
expect(channelTimestamps.has("1234.1")).toBe(true);
|
|
1994
2266
|
|
|
1995
|
-
expect(
|
|
1996
|
-
capturedHints?.some((hint) => hint.includes("joined an existing thread")),
|
|
1997
|
-
).not.toBe(true);
|
|
1998
|
-
expect(capturedSlackNotice).toContain("joined an existing thread");
|
|
1999
|
-
const contents = db.$client
|
|
2000
|
-
.prepare("SELECT content FROM messages")
|
|
2001
|
-
.all() as Array<{ content: string }>;
|
|
2002
|
-
expect(
|
|
2003
|
-
contents.some((row) => row.content.includes("Slack context note")),
|
|
2004
|
-
).toBe(false);
|
|
2267
|
+
expect(capturedHints ?? []).toEqual([]);
|
|
2005
2268
|
});
|
|
2006
2269
|
|
|
2007
2270
|
test("live Slack non-guardian passes raw displayContent while wrapping model content", async () => {
|
|
@@ -2009,6 +2272,14 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
2009
2272
|
const captured = await handleAndCaptureLiveSlackProcessMessage(
|
|
2010
2273
|
buildSlackChannelRequest("1700000000.000300", {
|
|
2011
2274
|
content: rawContent,
|
|
2275
|
+
clientTimezone: "America/Los_Angeles",
|
|
2276
|
+
sourceMetadata: {
|
|
2277
|
+
messageId: "1700000000.000300",
|
|
2278
|
+
chatType: "channel",
|
|
2279
|
+
timezone: "America/New_York",
|
|
2280
|
+
timezoneLabel: "Eastern Time",
|
|
2281
|
+
timezoneOffsetSeconds: -18000,
|
|
2282
|
+
},
|
|
2012
2283
|
}),
|
|
2013
2284
|
);
|
|
2014
2285
|
|
|
@@ -2020,10 +2291,27 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
2020
2291
|
expect(captured.options?.slackInbound?.actorExternalUserId).toBe(
|
|
2021
2292
|
HTTP_SLACK_USER_ID,
|
|
2022
2293
|
);
|
|
2294
|
+
expect(captured.options?.transport?.clientTimezone).toBe(
|
|
2295
|
+
"America/Los_Angeles",
|
|
2296
|
+
);
|
|
2297
|
+
expect(captured.options?.slackInbound?.actorTimezone).toBe(
|
|
2298
|
+
"America/New_York",
|
|
2299
|
+
);
|
|
2300
|
+
expect(captured.options?.slackInbound?.timestampTimezone).toBe(
|
|
2301
|
+
"America/Los_Angeles",
|
|
2302
|
+
);
|
|
2303
|
+
expect(captured.options?.slackInbound?.timestampTimezoneLabel).toBe("PT");
|
|
2304
|
+
expect(captured.options?.slackInbound?.speakerTimezoneLabel).toBe(
|
|
2305
|
+
"Eastern Time",
|
|
2306
|
+
);
|
|
2023
2307
|
|
|
2024
2308
|
const persisted = readMessagesByConversation(captured.conversationId);
|
|
2025
2309
|
expect(persisted).toHaveLength(1);
|
|
2026
2310
|
expect(persisted[0].content).toBe(rawContent);
|
|
2311
|
+
const [persistedSlackRow] = readPersistedSlackRows(captured.conversationId);
|
|
2312
|
+
expect(persistedSlackRow?.timestampTimezone).toBe("America/Los_Angeles");
|
|
2313
|
+
expect(persistedSlackRow?.timestampTimezoneLabel).toBe("PT");
|
|
2314
|
+
expect(persistedSlackRow?.speakerTimezoneLabel).toBe("Eastern Time");
|
|
2027
2315
|
});
|
|
2028
2316
|
|
|
2029
2317
|
test("live Slack attachment-only passes empty raw displayContent for persistence", async () => {
|
|
@@ -2236,6 +2524,66 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
|
|
|
2236
2524
|
expect(backfillThreadMock).not.toHaveBeenCalled();
|
|
2237
2525
|
});
|
|
2238
2526
|
|
|
2527
|
+
test("threaded Slack DMs use thread backfill instead of whole-DM backfill", async () => {
|
|
2528
|
+
const dmChannelId = "D0HTTPAPPTHREAD";
|
|
2529
|
+
const threadTs = "1700000000.000100";
|
|
2530
|
+
const inboundTs = "1700000000.000300";
|
|
2531
|
+
seedHttpActiveMember(dmChannelId);
|
|
2532
|
+
backfillDmMock.mockImplementation(async () => {
|
|
2533
|
+
throw new Error("whole-DM backfill should not run for threaded DMs");
|
|
2534
|
+
});
|
|
2535
|
+
backfillThreadMock.mockImplementation(async () => [
|
|
2536
|
+
makeBackfillMessage({
|
|
2537
|
+
id: threadTs,
|
|
2538
|
+
conversationId: dmChannelId,
|
|
2539
|
+
text: "app DM thread root",
|
|
2540
|
+
sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
|
|
2541
|
+
}),
|
|
2542
|
+
makeBackfillMessage({
|
|
2543
|
+
id: "1700000000.000200",
|
|
2544
|
+
conversationId: dmChannelId,
|
|
2545
|
+
text: "app DM thread context",
|
|
2546
|
+
threadId: threadTs,
|
|
2547
|
+
sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
|
|
2548
|
+
}),
|
|
2549
|
+
]);
|
|
2550
|
+
|
|
2551
|
+
const processMessage = async (
|
|
2552
|
+
conversationId: string,
|
|
2553
|
+
content: string,
|
|
2554
|
+
_attachmentIds?: string[],
|
|
2555
|
+
options?: SlackInboundProcessOptions,
|
|
2556
|
+
): Promise<{ messageId: string }> => ({
|
|
2557
|
+
messageId: persistSlackInboundFromProcessMessage(
|
|
2558
|
+
conversationId,
|
|
2559
|
+
content,
|
|
2560
|
+
options,
|
|
2561
|
+
),
|
|
2562
|
+
});
|
|
2563
|
+
setAdapterProcessMessage(processMessage);
|
|
2564
|
+
|
|
2565
|
+
const resp = await handleChannelInbound(
|
|
2566
|
+
buildSlackDmRequest(dmChannelId, inboundTs, {
|
|
2567
|
+
sourceMetadata: {
|
|
2568
|
+
messageId: inboundTs,
|
|
2569
|
+
threadId: threadTs,
|
|
2570
|
+
chatType: "im",
|
|
2571
|
+
},
|
|
2572
|
+
}),
|
|
2573
|
+
processMessage,
|
|
2574
|
+
TEST_BEARER_TOKEN,
|
|
2575
|
+
);
|
|
2576
|
+
|
|
2577
|
+
expect(resp.status).toBe(200);
|
|
2578
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2579
|
+
|
|
2580
|
+
expect(backfillDmMock).not.toHaveBeenCalled();
|
|
2581
|
+
expect(backfillThreadMock.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
2582
|
+
const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
|
|
2583
|
+
expect(calledChannel).toBe(dmChannelId);
|
|
2584
|
+
expect(calledThread).toBe(threadTs);
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2239
2587
|
test("second thread reply within the TTL window can fetch a newer bounded gap", async () => {
|
|
2240
2588
|
backfillThreadMock.mockImplementation(async () => [
|
|
2241
2589
|
makeBackfillMessage({ id: "5678.0", text: "parent" }),
|
|
@@ -51,6 +51,9 @@ let lastCheckArgs:
|
|
|
51
51
|
/** Optional override for getTool — lets tests supply skill-origin tools. */
|
|
52
52
|
let getToolOverride: ((name: string) => Tool | undefined) | undefined;
|
|
53
53
|
|
|
54
|
+
/** Optional override for getAllTools — lets tests supply a registry snapshot. */
|
|
55
|
+
let getAllToolsOverride: (() => Tool[]) | undefined;
|
|
56
|
+
|
|
54
57
|
/** Override the check() result for tests that need to trigger prompting. */
|
|
55
58
|
let checkResultOverride: { decision: string; reason: string } | undefined;
|
|
56
59
|
|
|
@@ -144,7 +147,7 @@ mock.module("../tools/registry.js", () => ({
|
|
|
144
147
|
execute: async () => fakeToolResult,
|
|
145
148
|
};
|
|
146
149
|
},
|
|
147
|
-
getAllTools: () => [],
|
|
150
|
+
getAllTools: () => (getAllToolsOverride ? getAllToolsOverride() : []),
|
|
148
151
|
}));
|
|
149
152
|
|
|
150
153
|
mock.module("../tools/shared/filesystem/path-policy.js", () => ({
|
|
@@ -179,6 +182,7 @@ describe("ToolExecutor allowedToolNames gating", () => {
|
|
|
179
182
|
fakeToolResult = { content: "ok", isError: false };
|
|
180
183
|
lastCheckArgs = undefined;
|
|
181
184
|
getToolOverride = undefined;
|
|
185
|
+
getAllToolsOverride = undefined;
|
|
182
186
|
checkResultOverride = undefined;
|
|
183
187
|
checkFnOverride = undefined;
|
|
184
188
|
cachedAssessmentOverride = undefined;
|
|
@@ -271,6 +275,89 @@ describe("ToolExecutor allowedToolNames gating", () => {
|
|
|
271
275
|
expect(result.content).toContain("file_read");
|
|
272
276
|
expect(result.content).toContain("not currently active");
|
|
273
277
|
});
|
|
278
|
+
|
|
279
|
+
test("unknown tool suggestion list is scoped to allowedToolNames", async () => {
|
|
280
|
+
// Surfacing every globally registered tool would leak tools active in
|
|
281
|
+
// other sessions and misdirect the model to tools it cannot invoke.
|
|
282
|
+
const makeTool = (name: string): Tool =>
|
|
283
|
+
({
|
|
284
|
+
name,
|
|
285
|
+
description: "test tool",
|
|
286
|
+
category: "test",
|
|
287
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
288
|
+
getDefinition: () => ({
|
|
289
|
+
name,
|
|
290
|
+
description: "test tool",
|
|
291
|
+
input_schema: { type: "object" as const, properties: {} },
|
|
292
|
+
}),
|
|
293
|
+
execute: async () => fakeToolResult,
|
|
294
|
+
}) as unknown as Tool;
|
|
295
|
+
getAllToolsOverride = () => [
|
|
296
|
+
makeTool("file_read"),
|
|
297
|
+
makeTool("file_write"),
|
|
298
|
+
makeTool("secret_skill_tool"),
|
|
299
|
+
];
|
|
300
|
+
const executor = new ToolExecutor(makePrompter());
|
|
301
|
+
const allowed = new Set(["file_read", "file_write"]);
|
|
302
|
+
const result = await executor.execute(
|
|
303
|
+
"unknown_tool",
|
|
304
|
+
{ foo: "bar" },
|
|
305
|
+
makeContext({ allowedToolNames: allowed }),
|
|
306
|
+
);
|
|
307
|
+
expect(result.isError).toBe(true);
|
|
308
|
+
expect(result.content).toBe(
|
|
309
|
+
"Unknown tool: unknown_tool. Available tools: file_read, file_write",
|
|
310
|
+
);
|
|
311
|
+
expect(result.content).not.toContain("secret_skill_tool");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("unknown tool name reports 'Unknown tool' even when allowedToolNames is set", async () => {
|
|
315
|
+
// Regression: a hallucinated tool name with allowedToolNames set used to
|
|
316
|
+
// hit the "not currently active. Load the skill that provides this tool
|
|
317
|
+
// first." gate, which sent the model chasing a nonexistent skill. The
|
|
318
|
+
// registry-lookup gate runs first now so the model sees the real list.
|
|
319
|
+
const executor = new ToolExecutor(makePrompter());
|
|
320
|
+
const allowed = new Set(["file_read"]);
|
|
321
|
+
const result = await executor.execute(
|
|
322
|
+
"unknown_tool",
|
|
323
|
+
{ foo: "bar" },
|
|
324
|
+
makeContext({ allowedToolNames: allowed }),
|
|
325
|
+
);
|
|
326
|
+
expect(result.isError).toBe(true);
|
|
327
|
+
expect(result.content).toContain("Unknown tool: unknown_tool");
|
|
328
|
+
expect(result.content).not.toContain("not currently active");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("inactive skill tool names the owning skill in the load hint", async () => {
|
|
332
|
+
const executor = new ToolExecutor(makePrompter());
|
|
333
|
+
getToolOverride = (name: string) => {
|
|
334
|
+
if (name !== "skill_tool_x") return undefined;
|
|
335
|
+
return {
|
|
336
|
+
name,
|
|
337
|
+
description: "tool from a skill",
|
|
338
|
+
category: "skill",
|
|
339
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
340
|
+
origin: "skill" as const,
|
|
341
|
+
ownerSkillId: "my-skill",
|
|
342
|
+
getDefinition: () => ({
|
|
343
|
+
name,
|
|
344
|
+
description: "tool from a skill",
|
|
345
|
+
input_schema: { type: "object" as const, properties: {} },
|
|
346
|
+
}),
|
|
347
|
+
execute: async () => fakeToolResult,
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
const allowed = new Set(["file_read"]);
|
|
351
|
+
const result = await executor.execute(
|
|
352
|
+
"skill_tool_x",
|
|
353
|
+
{},
|
|
354
|
+
makeContext({ allowedToolNames: allowed }),
|
|
355
|
+
);
|
|
356
|
+
expect(result.isError).toBe(true);
|
|
357
|
+
expect(result.content).toBe(
|
|
358
|
+
'Tool "skill_tool_x" is not currently active. Load the "my-skill" skill that provides this tool first.',
|
|
359
|
+
);
|
|
360
|
+
});
|
|
274
361
|
});
|
|
275
362
|
|
|
276
363
|
describe("ToolExecutor policy context plumbing", () => {
|
|
@@ -846,6 +933,7 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
|
|
|
846
933
|
// Import the real buildSanitizedEnv (not mocked) for baseline credential tests
|
|
847
934
|
const {
|
|
848
935
|
buildSanitizedEnv,
|
|
936
|
+
KATA_INJECTED_ENV_VARS,
|
|
849
937
|
KATA_SAFE_ENV_VARS,
|
|
850
938
|
SAFE_ENV_VARS,
|
|
851
939
|
ALWAYS_INJECTED_ENV_VARS,
|
|
@@ -910,6 +998,7 @@ describe("buildSanitizedEnv — baseline: credential exclusion", () => {
|
|
|
910
998
|
const allowed: string[] = [
|
|
911
999
|
...SAFE_ENV_VARS,
|
|
912
1000
|
...KATA_SAFE_ENV_VARS,
|
|
1001
|
+
...KATA_INJECTED_ENV_VARS,
|
|
913
1002
|
...ALWAYS_INJECTED_ENV_VARS,
|
|
914
1003
|
];
|
|
915
1004
|
const env = buildSanitizedEnv();
|