@vellumai/assistant 0.9.0 → 0.9.1-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getLogger } from "../../util/logger.js";
|
|
2
|
+
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const log = getLogger("migration-291");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Restores original platform-provided casing into address from external_user_id
|
|
8
|
+
* for channels where the raw platform ID is the canonical identity (Slack,
|
|
9
|
+
* Telegram, etc.). Phone and WhatsApp channels are excluded because their
|
|
10
|
+
* canonical form (E.164) may differ from the raw external_user_id.
|
|
11
|
+
*
|
|
12
|
+
* Idempotent: rows where address already equals external_user_id are no-ops.
|
|
13
|
+
*/
|
|
14
|
+
export function migrateContactChannelsRenormalizeAddresses(
|
|
15
|
+
database: DrizzleDb,
|
|
16
|
+
): void {
|
|
17
|
+
const raw = getSqliteFrom(database);
|
|
18
|
+
|
|
19
|
+
// Remove rows that would block normalization due to cross-column collisions.
|
|
20
|
+
raw.run(/*sql*/ `
|
|
21
|
+
DELETE FROM contact_channels
|
|
22
|
+
WHERE external_user_id IS NULL
|
|
23
|
+
AND id IN (
|
|
24
|
+
SELECT blocker.id
|
|
25
|
+
FROM contact_channels AS blocker
|
|
26
|
+
INNER JOIN contact_channels AS normalizer
|
|
27
|
+
ON normalizer.type = blocker.type
|
|
28
|
+
AND normalizer.external_user_id = blocker.address COLLATE NOCASE
|
|
29
|
+
AND normalizer.address != normalizer.external_user_id
|
|
30
|
+
AND normalizer.external_user_id IS NOT NULL
|
|
31
|
+
AND normalizer.id != blocker.id
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
// Non-phone, non-email channels: restore original platform casing from
|
|
36
|
+
// external_user_id. Phone/WhatsApp are excluded because their canonical
|
|
37
|
+
// form (E.164 with '+' prefix) may differ from the raw external_user_id.
|
|
38
|
+
const nonEmailResult = raw.run(/*sql*/ `
|
|
39
|
+
UPDATE OR IGNORE contact_channels
|
|
40
|
+
SET address = external_user_id
|
|
41
|
+
WHERE external_user_id IS NOT NULL
|
|
42
|
+
AND address != external_user_id
|
|
43
|
+
AND type NOT IN ('email', 'phone', 'whatsapp')
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
// Email channels: ensure address is lowercased (canonical per RFC 5321).
|
|
47
|
+
const emailResult = raw.run(/*sql*/ `
|
|
48
|
+
UPDATE OR IGNORE contact_channels
|
|
49
|
+
SET address = LOWER(external_user_id)
|
|
50
|
+
WHERE type = 'email'
|
|
51
|
+
AND external_user_id IS NOT NULL
|
|
52
|
+
AND address != LOWER(external_user_id)
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
const totalChanges = nonEmailResult.changes + emailResult.changes;
|
|
56
|
+
if (totalChanges > 0) {
|
|
57
|
+
log.info(
|
|
58
|
+
{ rowsUpdated: totalChanges },
|
|
59
|
+
"Re-normalized contact_channels addresses from external_user_id",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import { getSqliteFrom } from "../../db-connection.js";
|
|
7
|
+
import * as schema from "../../schema.js";
|
|
8
|
+
import { migrateContactChannelsRenormalizeAddresses } from "../291-contact-channels-renormalize-addresses.js";
|
|
9
|
+
|
|
10
|
+
interface ChannelRow {
|
|
11
|
+
id: string;
|
|
12
|
+
contact_id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
address: string;
|
|
15
|
+
external_user_id: string | null;
|
|
16
|
+
status: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createTestDb() {
|
|
20
|
+
const sqlite = new Database(":memory:");
|
|
21
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
22
|
+
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
23
|
+
return drizzle(sqlite, { schema });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function bootstrap(db: ReturnType<typeof createTestDb>): void {
|
|
27
|
+
const raw = getSqliteFrom(db);
|
|
28
|
+
raw.exec(/*sql*/ `
|
|
29
|
+
CREATE TABLE contacts (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
display_name TEXT NOT NULL,
|
|
32
|
+
created_at INTEGER NOT NULL,
|
|
33
|
+
updated_at INTEGER NOT NULL,
|
|
34
|
+
role TEXT NOT NULL DEFAULT 'contact',
|
|
35
|
+
contact_type TEXT NOT NULL DEFAULT 'human'
|
|
36
|
+
)
|
|
37
|
+
`);
|
|
38
|
+
raw.exec(/*sql*/ `
|
|
39
|
+
CREATE TABLE contact_channels (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
42
|
+
type TEXT NOT NULL,
|
|
43
|
+
address TEXT NOT NULL,
|
|
44
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
external_user_id TEXT,
|
|
46
|
+
external_chat_id TEXT,
|
|
47
|
+
status TEXT NOT NULL DEFAULT 'unverified',
|
|
48
|
+
policy TEXT NOT NULL DEFAULT 'allow',
|
|
49
|
+
interaction_count INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
updated_at INTEGER,
|
|
51
|
+
created_at INTEGER NOT NULL
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
raw.exec(/*sql*/ `
|
|
55
|
+
CREATE UNIQUE INDEX idx_contact_channels_type_address
|
|
56
|
+
ON contact_channels(type, address)
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function insertContact(raw: Database, id: string): void {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
raw
|
|
63
|
+
.query(
|
|
64
|
+
`INSERT INTO contacts (id, display_name, created_at, updated_at) VALUES (?, ?, ?, ?)`,
|
|
65
|
+
)
|
|
66
|
+
.run(id, `Contact ${id}`, now, now);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function insertChannel(
|
|
70
|
+
raw: Database,
|
|
71
|
+
opts: {
|
|
72
|
+
id: string;
|
|
73
|
+
contactId: string;
|
|
74
|
+
type: string;
|
|
75
|
+
address: string;
|
|
76
|
+
externalUserId: string | null;
|
|
77
|
+
status: string;
|
|
78
|
+
},
|
|
79
|
+
): void {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
raw
|
|
82
|
+
.query(
|
|
83
|
+
`INSERT INTO contact_channels (id, contact_id, type, address, external_user_id, status, updated_at, created_at)
|
|
84
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
85
|
+
)
|
|
86
|
+
.run(
|
|
87
|
+
opts.id,
|
|
88
|
+
opts.contactId,
|
|
89
|
+
opts.type,
|
|
90
|
+
opts.address,
|
|
91
|
+
opts.externalUserId,
|
|
92
|
+
opts.status,
|
|
93
|
+
now,
|
|
94
|
+
now,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("migration 291 — renormalize addresses", () => {
|
|
99
|
+
test("restores Slack address casing from external_user_id", () => {
|
|
100
|
+
const db = createTestDb();
|
|
101
|
+
bootstrap(db);
|
|
102
|
+
const raw = getSqliteFrom(db);
|
|
103
|
+
|
|
104
|
+
insertContact(raw, "c1");
|
|
105
|
+
insertChannel(raw, {
|
|
106
|
+
id: "ch1",
|
|
107
|
+
contactId: "c1",
|
|
108
|
+
type: "slack",
|
|
109
|
+
address: "u12345abc",
|
|
110
|
+
externalUserId: "U12345ABC",
|
|
111
|
+
status: "active",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
115
|
+
|
|
116
|
+
const rows = raw
|
|
117
|
+
.prepare("SELECT * FROM contact_channels")
|
|
118
|
+
.all() as ChannelRow[];
|
|
119
|
+
expect(rows).toHaveLength(1);
|
|
120
|
+
expect(rows[0].address).toBe("U12345ABC");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("preserves lowercase email addresses (does not overwrite with mixed-case external_user_id)", () => {
|
|
124
|
+
const db = createTestDb();
|
|
125
|
+
bootstrap(db);
|
|
126
|
+
const raw = getSqliteFrom(db);
|
|
127
|
+
|
|
128
|
+
insertContact(raw, "c1");
|
|
129
|
+
insertChannel(raw, {
|
|
130
|
+
id: "ch1",
|
|
131
|
+
contactId: "c1",
|
|
132
|
+
type: "email",
|
|
133
|
+
address: "user@example.com",
|
|
134
|
+
// generic-examples:ignore-next-line — reason: testing mixed-case email normalization
|
|
135
|
+
externalUserId: "User@Example.com",
|
|
136
|
+
status: "active",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
140
|
+
|
|
141
|
+
const rows = raw
|
|
142
|
+
.prepare("SELECT * FROM contact_channels")
|
|
143
|
+
.all() as ChannelRow[];
|
|
144
|
+
expect(rows).toHaveLength(1);
|
|
145
|
+
expect(rows[0].address).toBe("user@example.com");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("lowercases email address if it was stored incorrectly", () => {
|
|
149
|
+
const db = createTestDb();
|
|
150
|
+
bootstrap(db);
|
|
151
|
+
const raw = getSqliteFrom(db);
|
|
152
|
+
|
|
153
|
+
insertContact(raw, "c1");
|
|
154
|
+
insertChannel(raw, {
|
|
155
|
+
id: "ch1",
|
|
156
|
+
contactId: "c1",
|
|
157
|
+
type: "email",
|
|
158
|
+
// generic-examples:ignore-next-line — reason: testing mixed-case email stored incorrectly
|
|
159
|
+
address: "User@Example.com",
|
|
160
|
+
// generic-examples:ignore-next-line — reason: testing mixed-case email stored incorrectly
|
|
161
|
+
externalUserId: "User@Example.com",
|
|
162
|
+
status: "active",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
166
|
+
|
|
167
|
+
const rows = raw
|
|
168
|
+
.prepare("SELECT * FROM contact_channels")
|
|
169
|
+
.all() as ChannelRow[];
|
|
170
|
+
expect(rows).toHaveLength(1);
|
|
171
|
+
expect(rows[0].address).toBe("user@example.com");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("removes cross-column blocker before normalization", () => {
|
|
175
|
+
const db = createTestDb();
|
|
176
|
+
bootstrap(db);
|
|
177
|
+
const raw = getSqliteFrom(db);
|
|
178
|
+
|
|
179
|
+
insertContact(raw, "c1");
|
|
180
|
+
insertContact(raw, "c2");
|
|
181
|
+
// Blocker: NULL external_user_id, address matches normalizer's external_user_id
|
|
182
|
+
insertChannel(raw, {
|
|
183
|
+
id: "blocker",
|
|
184
|
+
contactId: "c1",
|
|
185
|
+
type: "slack",
|
|
186
|
+
address: "u12345abc",
|
|
187
|
+
externalUserId: null,
|
|
188
|
+
status: "unverified",
|
|
189
|
+
});
|
|
190
|
+
// Normalizer: has external_user_id that will become its address
|
|
191
|
+
insertChannel(raw, {
|
|
192
|
+
id: "normalizer",
|
|
193
|
+
contactId: "c2",
|
|
194
|
+
type: "slack",
|
|
195
|
+
address: "old-address",
|
|
196
|
+
externalUserId: "U12345ABC",
|
|
197
|
+
status: "active",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
201
|
+
|
|
202
|
+
const rows = raw
|
|
203
|
+
.prepare("SELECT * FROM contact_channels ORDER BY id")
|
|
204
|
+
.all() as ChannelRow[];
|
|
205
|
+
expect(rows).toHaveLength(1);
|
|
206
|
+
expect(rows[0].id).toBe("normalizer");
|
|
207
|
+
expect(rows[0].address).toBe("U12345ABC");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("idempotent — safe to run twice", () => {
|
|
211
|
+
const db = createTestDb();
|
|
212
|
+
bootstrap(db);
|
|
213
|
+
const raw = getSqliteFrom(db);
|
|
214
|
+
|
|
215
|
+
insertContact(raw, "c1");
|
|
216
|
+
insertChannel(raw, {
|
|
217
|
+
id: "ch1",
|
|
218
|
+
contactId: "c1",
|
|
219
|
+
type: "slack",
|
|
220
|
+
address: "u12345abc",
|
|
221
|
+
externalUserId: "U12345ABC",
|
|
222
|
+
status: "active",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
226
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
227
|
+
|
|
228
|
+
const rows = raw
|
|
229
|
+
.prepare("SELECT * FROM contact_channels")
|
|
230
|
+
.all() as ChannelRow[];
|
|
231
|
+
expect(rows).toHaveLength(1);
|
|
232
|
+
expect(rows[0].address).toBe("U12345ABC");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("does not overwrite canonical E.164 phone address with raw external_user_id", () => {
|
|
236
|
+
const db = createTestDb();
|
|
237
|
+
bootstrap(db);
|
|
238
|
+
const raw = getSqliteFrom(db);
|
|
239
|
+
|
|
240
|
+
insertContact(raw, "c1");
|
|
241
|
+
// Phone channel whose address was canonicalized to E.164 (+1 prefix)
|
|
242
|
+
// but external_user_id still holds the raw 10-digit number.
|
|
243
|
+
insertChannel(raw, {
|
|
244
|
+
id: "ch1",
|
|
245
|
+
contactId: "c1",
|
|
246
|
+
type: "phone",
|
|
247
|
+
// generic-examples:ignore-next-line — reason: testing E.164 canonicalization preservation
|
|
248
|
+
address: "+15550101234",
|
|
249
|
+
// generic-examples:ignore-next-line — reason: testing raw 10-digit phone before E.164 normalization
|
|
250
|
+
externalUserId: "5550101234",
|
|
251
|
+
status: "active",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
255
|
+
|
|
256
|
+
const rows = raw
|
|
257
|
+
.prepare("SELECT * FROM contact_channels")
|
|
258
|
+
.all() as ChannelRow[];
|
|
259
|
+
expect(rows).toHaveLength(1);
|
|
260
|
+
// generic-examples:ignore-next-line — reason: verifying E.164 address preserved
|
|
261
|
+
expect(rows[0].address).toBe("+15550101234");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("does not overwrite canonical WhatsApp address with raw external_user_id", () => {
|
|
265
|
+
const db = createTestDb();
|
|
266
|
+
bootstrap(db);
|
|
267
|
+
const raw = getSqliteFrom(db);
|
|
268
|
+
|
|
269
|
+
insertContact(raw, "c1");
|
|
270
|
+
insertChannel(raw, {
|
|
271
|
+
id: "ch1",
|
|
272
|
+
contactId: "c1",
|
|
273
|
+
type: "whatsapp",
|
|
274
|
+
address: "+447911123456",
|
|
275
|
+
externalUserId: "447911123456",
|
|
276
|
+
status: "active",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
280
|
+
|
|
281
|
+
const rows = raw
|
|
282
|
+
.prepare("SELECT * FROM contact_channels")
|
|
283
|
+
.all() as ChannelRow[];
|
|
284
|
+
expect(rows).toHaveLength(1);
|
|
285
|
+
expect(rows[0].address).toBe("+447911123456");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("no-op when address already matches external_user_id", () => {
|
|
289
|
+
const db = createTestDb();
|
|
290
|
+
bootstrap(db);
|
|
291
|
+
const raw = getSqliteFrom(db);
|
|
292
|
+
|
|
293
|
+
insertContact(raw, "c1");
|
|
294
|
+
insertChannel(raw, {
|
|
295
|
+
id: "ch1",
|
|
296
|
+
contactId: "c1",
|
|
297
|
+
type: "slack",
|
|
298
|
+
address: "U12345ABC",
|
|
299
|
+
externalUserId: "U12345ABC",
|
|
300
|
+
status: "active",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
migrateContactChannelsRenormalizeAddresses(db);
|
|
304
|
+
|
|
305
|
+
const rows = raw
|
|
306
|
+
.prepare("SELECT * FROM contact_channels")
|
|
307
|
+
.all() as ChannelRow[];
|
|
308
|
+
expect(rows).toHaveLength(1);
|
|
309
|
+
expect(rows[0].address).toBe("U12345ABC");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import * as schema from "../../schema.js";
|
|
7
|
+
import { type MigrationStep, runMigrationSteps } from "../run-migrations.js";
|
|
8
|
+
|
|
9
|
+
function createTestDb() {
|
|
10
|
+
const sqlite = new Database(":memory:");
|
|
11
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
12
|
+
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
13
|
+
return drizzle(sqlite, { schema });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("runMigrationSteps — checkpointing", () => {
|
|
17
|
+
// Skipped: documents the post-checkpointing contract and fails against the
|
|
18
|
+
// current behavior, which re-runs every forward step on every boot. Promote
|
|
19
|
+
// to `test` when true checkpointing of initial migrations lands.
|
|
20
|
+
test.skip("does not re-execute a step that was already applied on a prior run", () => {
|
|
21
|
+
/**
|
|
22
|
+
* Booting an already-migrated database must not re-run forward migration
|
|
23
|
+
* steps. With true checkpointing each step's body executes at most once
|
|
24
|
+
* across boots; without it every step re-runs on every startup — the
|
|
25
|
+
* ~200-step re-probe that floors daemon startup at ~30s.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// GIVEN three dummy migration steps that count how often their body runs
|
|
29
|
+
const calls = { a: 0, b: 0, c: 0 };
|
|
30
|
+
const steps: MigrationStep[] = [
|
|
31
|
+
function dummyMigrationA() {
|
|
32
|
+
calls.a++;
|
|
33
|
+
},
|
|
34
|
+
function dummyMigrationB() {
|
|
35
|
+
calls.b++;
|
|
36
|
+
},
|
|
37
|
+
function dummyMigrationC() {
|
|
38
|
+
calls.c++;
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// AND a database that has already been migrated once
|
|
43
|
+
const db = createTestDb();
|
|
44
|
+
runMigrationSteps(db, steps);
|
|
45
|
+
|
|
46
|
+
// WHEN the same steps run again against the already-migrated database
|
|
47
|
+
runMigrationSteps(db, steps);
|
|
48
|
+
|
|
49
|
+
// THEN no step body executes a second time
|
|
50
|
+
expect(calls).toEqual({ a: 1, b: 1, c: 1 });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -282,6 +282,7 @@ export { migrateConversationOriginChannelIndex } from "./287-conversation-origin
|
|
|
282
282
|
export { migrateBackfillOriginChannelFromBindings } from "./288-backfill-origin-channel-from-bindings.js";
|
|
283
283
|
export { migrateContactChannelsUniqueExtUser } from "./289-contact-channels-unique-ext-user.js";
|
|
284
284
|
export { migrateScheduleCapabilities } from "./290-schedule-capabilities.js";
|
|
285
|
+
export { migrateContactChannelsRenormalizeAddresses } from "./291-contact-channels-renormalize-addresses.js";
|
|
285
286
|
export {
|
|
286
287
|
MIGRATION_REGISTRY,
|
|
287
288
|
type MigrationRegistryEntry,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getLogger } from "../../util/logger.js";
|
|
2
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const log = getLogger("db-init");
|
|
5
|
+
|
|
6
|
+
/** A single forward migration step, identified for logging by its `.name`. */
|
|
7
|
+
export type MigrationStep = (database: DrizzleDb) => void;
|
|
8
|
+
|
|
9
|
+
export interface MigrationRunResult {
|
|
10
|
+
/** Steps whose body threw. */
|
|
11
|
+
failed: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run the ordered list of forward migration steps against the database.
|
|
16
|
+
*
|
|
17
|
+
* Individual step failures are caught and logged so one broken migration does
|
|
18
|
+
* not prevent independent later ones from succeeding.
|
|
19
|
+
*/
|
|
20
|
+
export function runMigrationSteps(
|
|
21
|
+
database: DrizzleDb,
|
|
22
|
+
steps: MigrationStep[],
|
|
23
|
+
): MigrationRunResult {
|
|
24
|
+
const failed: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (const step of steps) {
|
|
27
|
+
try {
|
|
28
|
+
log.debug({ migration: step.name }, `Starting migration: ${step.name}`);
|
|
29
|
+
step(database);
|
|
30
|
+
log.debug({ migration: step.name }, `Migration succeeded: ${step.name}`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
failed.push(step.name);
|
|
33
|
+
log.error(
|
|
34
|
+
{ err, migration: step.name },
|
|
35
|
+
`Migration failed: ${step.name}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { failed };
|
|
41
|
+
}
|
|
@@ -220,7 +220,7 @@ export function validateMigrationState(
|
|
|
220
220
|
// registry entry — these are from a newer version of the daemon.
|
|
221
221
|
//
|
|
222
222
|
// The memory_checkpoints table is a general-purpose key-value store also
|
|
223
|
-
// used by non-migration subsystems (e.g., "
|
|
223
|
+
// used by non-migration subsystems (e.g., "empty_state:greeting:text",
|
|
224
224
|
// "conversation_starters:item_count_at_last_gen"). Filter to only keys
|
|
225
225
|
// that follow migration naming conventions before comparing against the
|
|
226
226
|
// registry to avoid false-positive warnings.
|
|
@@ -42,10 +42,6 @@ export const contactChannels = sqliteTable(
|
|
|
42
42
|
createdAt: integer("created_at").notNull(),
|
|
43
43
|
},
|
|
44
44
|
(table) => [
|
|
45
|
-
index("idx_contact_channels_type_ext_user").on(
|
|
46
|
-
table.type,
|
|
47
|
-
table.externalUserId,
|
|
48
|
-
),
|
|
49
45
|
index("idx_contact_channels_type_ext_chat").on(
|
|
50
46
|
table.type,
|
|
51
47
|
table.externalChatId,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared adapter utilities — functions used by multiple channel adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isConversationSeedSane } from "../conversation-seed-composer.js";
|
|
6
|
+
import { nonEmpty } from "../notification-utils.js";
|
|
7
|
+
import type { ChannelDeliveryPayload } from "../types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the primary message text for a notification delivery.
|
|
11
|
+
*
|
|
12
|
+
* Cascade: deliveryText → conversationSeedMessage → body → title → event name.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveMessageText(payload: ChannelDeliveryPayload): string {
|
|
15
|
+
const deliveryText = nonEmpty(payload.copy.deliveryText);
|
|
16
|
+
if (deliveryText) return deliveryText;
|
|
17
|
+
|
|
18
|
+
if (isConversationSeedSane(payload.copy.conversationSeedMessage)) {
|
|
19
|
+
return payload.copy.conversationSeedMessage.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const body = nonEmpty(payload.copy.body);
|
|
23
|
+
if (body) return body;
|
|
24
|
+
|
|
25
|
+
const title = nonEmpty(payload.copy.title);
|
|
26
|
+
if (title) return title;
|
|
27
|
+
|
|
28
|
+
return payload.sourceEventName.replace(/[._]/g, " ");
|
|
29
|
+
}
|
|
@@ -20,14 +20,13 @@ import {
|
|
|
20
20
|
buildAccessRequestWarnings,
|
|
21
21
|
buildSlackMessagePermalink,
|
|
22
22
|
isSlackDmConversation,
|
|
23
|
-
parseAccessRequestPayload,
|
|
24
23
|
type ParsedAccessRequestPayload,
|
|
25
24
|
} from "../access-request-copy.js";
|
|
26
|
-
import { isConversationSeedSane } from "../conversation-seed-composer.js";
|
|
27
25
|
import {
|
|
28
26
|
nonEmpty,
|
|
29
27
|
sanitizeIdentityField,
|
|
30
28
|
sanitizeMessagePreview,
|
|
29
|
+
truncate,
|
|
31
30
|
} from "../notification-utils.js";
|
|
32
31
|
import type {
|
|
33
32
|
ChannelAdapter,
|
|
@@ -38,36 +37,10 @@ import type {
|
|
|
38
37
|
DeliveryResult,
|
|
39
38
|
NotificationChannel,
|
|
40
39
|
} from "../types.js";
|
|
40
|
+
import { resolveMessageText } from "./shared.js";
|
|
41
41
|
|
|
42
42
|
const log = getLogger("notif-adapter-slack");
|
|
43
43
|
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Text resolution
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
function resolveSlackMessageText(payload: ChannelDeliveryPayload): string {
|
|
49
|
-
const deliveryText = nonEmpty(payload.copy.deliveryText);
|
|
50
|
-
if (deliveryText) return deliveryText;
|
|
51
|
-
|
|
52
|
-
if (isConversationSeedSane(payload.copy.conversationSeedMessage)) {
|
|
53
|
-
return payload.copy.conversationSeedMessage.trim();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const body = nonEmpty(payload.copy.body);
|
|
57
|
-
if (body) return body;
|
|
58
|
-
|
|
59
|
-
const title = nonEmpty(payload.copy.title);
|
|
60
|
-
if (title) return title;
|
|
61
|
-
|
|
62
|
-
return payload.sourceEventName.replace(/[._]/g, " ");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Truncate to `maxLength`, appending "…" when exceeded. */
|
|
66
|
-
function truncate(text: string, maxLength: number): string {
|
|
67
|
-
if (text.length <= maxLength) return text;
|
|
68
|
-
return text.slice(0, maxLength - 1) + "…";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
44
|
// ---------------------------------------------------------------------------
|
|
72
45
|
// Slack Card block builders for approval notifications
|
|
73
46
|
// ---------------------------------------------------------------------------
|
|
@@ -193,7 +166,7 @@ function buildAccessRequestCardBlocks(
|
|
|
193
166
|
payload: ChannelDeliveryPayload,
|
|
194
167
|
): unknown[] {
|
|
195
168
|
const approval = payload.approvalContext!;
|
|
196
|
-
const p =
|
|
169
|
+
const p = payload.accessRequestContext!;
|
|
197
170
|
const blocks: unknown[] = [];
|
|
198
171
|
|
|
199
172
|
const subtitle = buildAccessRequestSubtitle(p);
|
|
@@ -314,7 +287,7 @@ function buildApprovalNotificationBlocks(
|
|
|
314
287
|
): unknown[] {
|
|
315
288
|
if (
|
|
316
289
|
payload.sourceEventName === "ingress.access_request" &&
|
|
317
|
-
payload.
|
|
290
|
+
payload.accessRequestContext != null
|
|
318
291
|
) {
|
|
319
292
|
return buildAccessRequestCardBlocks(payload);
|
|
320
293
|
}
|
|
@@ -345,7 +318,7 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
345
318
|
};
|
|
346
319
|
}
|
|
347
320
|
|
|
348
|
-
const messageText =
|
|
321
|
+
const messageText = resolveMessageText(payload);
|
|
349
322
|
|
|
350
323
|
try {
|
|
351
324
|
const result = payload.approvalContext
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
import { sendTelegramReply } from "../../messaging/providers/telegram-bot/send.js";
|
|
12
12
|
import { ConfigError } from "../../util/errors.js";
|
|
13
13
|
import { getLogger } from "../../util/logger.js";
|
|
14
|
-
import { isConversationSeedSane } from "../conversation-seed-composer.js";
|
|
15
|
-
import { nonEmpty } from "../notification-utils.js";
|
|
16
14
|
import type {
|
|
17
15
|
ChannelAdapter,
|
|
18
16
|
ChannelDeliveryPayload,
|
|
@@ -20,26 +18,10 @@ import type {
|
|
|
20
18
|
DeliveryResult,
|
|
21
19
|
NotificationChannel,
|
|
22
20
|
} from "../types.js";
|
|
21
|
+
import { resolveMessageText } from "./shared.js";
|
|
23
22
|
|
|
24
23
|
const log = getLogger("notif-adapter-telegram");
|
|
25
24
|
|
|
26
|
-
function resolveTelegramMessageText(payload: ChannelDeliveryPayload): string {
|
|
27
|
-
const deliveryText = nonEmpty(payload.copy.deliveryText);
|
|
28
|
-
if (deliveryText) return deliveryText;
|
|
29
|
-
|
|
30
|
-
if (isConversationSeedSane(payload.copy.conversationSeedMessage)) {
|
|
31
|
-
return payload.copy.conversationSeedMessage.trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const body = nonEmpty(payload.copy.body);
|
|
35
|
-
if (body) return body;
|
|
36
|
-
|
|
37
|
-
const title = nonEmpty(payload.copy.title);
|
|
38
|
-
if (title) return title;
|
|
39
|
-
|
|
40
|
-
return payload.sourceEventName.replace(/[._]/g, " ");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
25
|
export class TelegramAdapter implements ChannelAdapter {
|
|
44
26
|
readonly channel: NotificationChannel = "telegram";
|
|
45
27
|
|
|
@@ -59,7 +41,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
59
41
|
};
|
|
60
42
|
}
|
|
61
43
|
|
|
62
|
-
const messageText =
|
|
44
|
+
const messageText = resolveMessageText(payload);
|
|
63
45
|
const approval = payload.approvalContext;
|
|
64
46
|
|
|
65
47
|
try {
|