@vellumai/assistant 0.4.30 → 0.4.32

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.
Files changed (194) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +14 -8
  3. package/README.md +2 -2
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +1 -4
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  8. package/src/__tests__/anthropic-provider.test.ts +86 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/checker.test.ts +37 -98
  11. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
  12. package/src/__tests__/config-schema.test.ts +6 -14
  13. package/src/__tests__/conflict-policy.test.ts +76 -0
  14. package/src/__tests__/conflict-store.test.ts +14 -20
  15. package/src/__tests__/contacts-tools.test.ts +8 -61
  16. package/src/__tests__/contradiction-checker.test.ts +5 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  19. package/src/__tests__/followup-tools.test.ts +0 -30
  20. package/src/__tests__/gemini-provider.test.ts +79 -1
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  22. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  23. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  24. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  25. package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
  26. package/src/__tests__/memory-regressions.test.ts +6 -6
  27. package/src/__tests__/openai-provider.test.ts +82 -0
  28. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  29. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  30. package/src/__tests__/recurrence-types.test.ts +0 -15
  31. package/src/__tests__/registry.test.ts +0 -10
  32. package/src/__tests__/schedule-tools.test.ts +28 -44
  33. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  34. package/src/__tests__/session-agent-loop.test.ts +0 -2
  35. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  36. package/src/__tests__/session-profile-injection.test.ts +0 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  38. package/src/__tests__/session-skill-tools.test.ts +0 -49
  39. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  40. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  41. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  42. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  43. package/src/__tests__/task-management-tools.test.ts +111 -0
  44. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  45. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  46. package/src/__tests__/twilio-config.test.ts +0 -3
  47. package/src/amazon/session.ts +30 -91
  48. package/src/approvals/guardian-decision-primitive.ts +11 -7
  49. package/src/approvals/guardian-request-resolvers.ts +5 -3
  50. package/src/calls/call-controller.ts +423 -571
  51. package/src/calls/finalize-call.ts +20 -0
  52. package/src/calls/relay-access-wait.ts +340 -0
  53. package/src/calls/relay-server.ts +269 -899
  54. package/src/calls/relay-setup-router.ts +307 -0
  55. package/src/calls/relay-verification.ts +280 -0
  56. package/src/calls/twilio-config.ts +1 -8
  57. package/src/calls/voice-control-protocol.ts +184 -0
  58. package/src/calls/voice-session-bridge.ts +1 -8
  59. package/src/config/agent-schema.ts +1 -1
  60. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  61. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  62. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  63. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  64. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  65. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  66. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  67. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  68. package/src/config/bundled-tool-registry.ts +0 -5
  69. package/src/config/core-schema.ts +1 -1
  70. package/src/config/env.ts +0 -10
  71. package/src/config/feature-flag-registry.json +1 -1
  72. package/src/config/loader.ts +19 -0
  73. package/src/config/memory-schema.ts +0 -10
  74. package/src/config/schema.ts +2 -2
  75. package/src/config/system-prompt.ts +6 -0
  76. package/src/contacts/contact-store.ts +36 -62
  77. package/src/contacts/contacts-write.ts +14 -3
  78. package/src/contacts/types.ts +9 -4
  79. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  80. package/src/daemon/handlers/contacts.ts +2 -2
  81. package/src/daemon/handlers/guardian-actions.ts +1 -1
  82. package/src/daemon/handlers/session-history.ts +398 -0
  83. package/src/daemon/handlers/session-user-message.ts +982 -0
  84. package/src/daemon/handlers/sessions.ts +9 -1337
  85. package/src/daemon/ipc-contract/contacts.ts +2 -2
  86. package/src/daemon/ipc-contract/sessions.ts +0 -6
  87. package/src/daemon/ipc-contract-inventory.json +0 -1
  88. package/src/daemon/lifecycle.ts +0 -29
  89. package/src/daemon/session-agent-loop.ts +1 -45
  90. package/src/daemon/session-conflict-gate.ts +21 -82
  91. package/src/daemon/session-memory.ts +7 -52
  92. package/src/daemon/session-process.ts +3 -1
  93. package/src/daemon/session-runtime-assembly.ts +18 -35
  94. package/src/heartbeat/heartbeat-service.ts +5 -1
  95. package/src/home-base/app-link-store.ts +0 -7
  96. package/src/memory/conflict-intent.ts +3 -6
  97. package/src/memory/conflict-policy.ts +34 -0
  98. package/src/memory/conflict-store.ts +10 -18
  99. package/src/memory/contradiction-checker.ts +2 -2
  100. package/src/memory/conversation-attention-store.ts +1 -1
  101. package/src/memory/conversation-store.ts +0 -51
  102. package/src/memory/db-init.ts +8 -0
  103. package/src/memory/job-handlers/conflict.ts +24 -7
  104. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  105. package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
  106. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  107. package/src/memory/migrations/index.ts +2 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/recall-cache.ts +0 -5
  110. package/src/memory/schema/calls.ts +274 -0
  111. package/src/memory/schema/contacts.ts +125 -0
  112. package/src/memory/schema/conversations.ts +129 -0
  113. package/src/memory/schema/guardian.ts +172 -0
  114. package/src/memory/schema/index.ts +8 -0
  115. package/src/memory/schema/infrastructure.ts +205 -0
  116. package/src/memory/schema/memory-core.ts +196 -0
  117. package/src/memory/schema/notifications.ts +191 -0
  118. package/src/memory/schema/tasks.ts +78 -0
  119. package/src/memory/schema.ts +1 -1402
  120. package/src/memory/slack-thread-store.ts +0 -69
  121. package/src/messaging/index.ts +0 -1
  122. package/src/messaging/types.ts +0 -38
  123. package/src/notifications/decisions-store.ts +2 -105
  124. package/src/notifications/deliveries-store.ts +0 -11
  125. package/src/notifications/preferences-store.ts +1 -58
  126. package/src/permissions/checker.ts +6 -17
  127. package/src/providers/anthropic/client.ts +6 -2
  128. package/src/providers/gemini/client.ts +13 -2
  129. package/src/providers/managed-proxy/constants.ts +55 -0
  130. package/src/providers/managed-proxy/context.ts +77 -0
  131. package/src/providers/registry.ts +112 -0
  132. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  133. package/src/runtime/guardian-action-service.ts +3 -2
  134. package/src/runtime/guardian-outbound-actions.ts +3 -3
  135. package/src/runtime/guardian-reply-router.ts +4 -4
  136. package/src/runtime/http-server.ts +83 -710
  137. package/src/runtime/http-types.ts +0 -16
  138. package/src/runtime/middleware/auth.ts +0 -12
  139. package/src/runtime/routes/app-routes.ts +33 -0
  140. package/src/runtime/routes/approval-routes.ts +32 -0
  141. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  142. package/src/runtime/routes/attachment-routes.ts +32 -0
  143. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  144. package/src/runtime/routes/call-routes.ts +41 -0
  145. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  146. package/src/runtime/routes/channel-routes.ts +70 -0
  147. package/src/runtime/routes/contact-routes.ts +371 -29
  148. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  149. package/src/runtime/routes/conversation-routes.ts +192 -194
  150. package/src/runtime/routes/debug-routes.ts +15 -0
  151. package/src/runtime/routes/events-routes.ts +16 -0
  152. package/src/runtime/routes/global-search-routes.ts +17 -2
  153. package/src/runtime/routes/guardian-action-routes.ts +23 -1
  154. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  155. package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
  156. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  157. package/src/runtime/routes/identity-routes.ts +20 -0
  158. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  159. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  160. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  161. package/src/runtime/routes/integration-routes.ts +83 -0
  162. package/src/runtime/routes/invite-routes.ts +31 -0
  163. package/src/runtime/routes/migration-routes.ts +47 -17
  164. package/src/runtime/routes/pairing-routes.ts +18 -0
  165. package/src/runtime/routes/secret-routes.ts +20 -0
  166. package/src/runtime/routes/surface-action-routes.ts +26 -0
  167. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  168. package/src/runtime/routes/twilio-routes.ts +79 -0
  169. package/src/schedule/recurrence-types.ts +1 -11
  170. package/src/tools/followups/followup_create.ts +9 -3
  171. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  172. package/src/tools/memory/definitions.ts +0 -6
  173. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  174. package/src/tools/schedule/create.ts +1 -3
  175. package/src/tools/schedule/update.ts +9 -6
  176. package/src/twitter/session.ts +29 -77
  177. package/src/util/cookie-session.ts +114 -0
  178. package/src/workspace/git-service.ts +6 -4
  179. package/src/__tests__/conversation-routes.test.ts +0 -99
  180. package/src/__tests__/get-weather.test.ts +0 -393
  181. package/src/__tests__/task-tools.test.ts +0 -685
  182. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  183. package/src/autonomy/autonomy-resolver.ts +0 -62
  184. package/src/autonomy/autonomy-store.ts +0 -138
  185. package/src/autonomy/disposition-mapper.ts +0 -31
  186. package/src/autonomy/index.ts +0 -11
  187. package/src/autonomy/types.ts +0 -43
  188. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  189. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  190. package/src/config/bundled-skills/weather/icon.svg +0 -24
  191. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  192. package/src/contacts/startup-migration.ts +0 -21
  193. package/src/messaging/triage-engine.ts +0 -344
  194. package/src/tools/weather/service.ts +0 -712
@@ -1,21 +0,0 @@
1
- /**
2
- * Legacy startup migration stub.
3
- *
4
- * Previously this populated the contacts table from legacy guardian-binding
5
- * and ingress-member rows on daemon boot. That sync is now handled by the
6
- * DB migration (131-drop-legacy-member-guardian-tables) which safety-syncs
7
- * remaining data then drops both legacy tables. This stub is kept to avoid
8
- * breaking callers; it simply no-ops.
9
- */
10
-
11
- import { getLogger } from "../util/logger.js";
12
-
13
- const log = getLogger("contacts-startup-migration");
14
-
15
- /**
16
- * No-op — legacy tables have been dropped. The DB migration
17
- * (131-drop-legacy-member-guardian-tables) performed the final sync.
18
- */
19
- export function migrateContactsFromLegacyTables(_assistantId: string): void {
20
- log.debug("Legacy startup migration skipped — tables already dropped");
21
- }
@@ -1,344 +0,0 @@
1
- /**
2
- * Channel-agnostic message triage engine.
3
- *
4
- * Classifies an inbound message by combining sender context from the
5
- * contact graph, matching action playbooks, and an LLM call
6
- * for final classification. Results are persisted to the triageResults
7
- * table for accuracy review.
8
- */
9
-
10
- import { and, desc, eq, isNull } from "drizzle-orm";
11
- import { v4 as uuid } from "uuid";
12
-
13
- import { findContactByAddress } from "../contacts/contact-store.js";
14
- import type { ContactWithChannels } from "../contacts/types.js";
15
- import { getDb } from "../memory/db.js";
16
- import { memoryItems, triageResults } from "../memory/schema.js";
17
- import type { Playbook } from "../playbooks/types.js";
18
- import { parsePlaybookStatement } from "../playbooks/types.js";
19
- import {
20
- createTimeout,
21
- extractToolUse,
22
- getConfiguredProvider,
23
- userMessage,
24
- } from "../providers/provider-send-message.js";
25
- import { getLogger } from "../util/logger.js";
26
- import { truncate } from "../util/truncate.js";
27
- import type { InboundMessage, TriageResult } from "./types.js";
28
- import { DEFAULT_TRIAGE_CATEGORIES } from "./types.js";
29
-
30
- const log = getLogger("triage-engine");
31
-
32
- const TRIAGE_MODEL_INTENT = "latency-optimized" as const;
33
- const TRIAGE_CLASSIFICATION_TIMEOUT_MS = 15_000;
34
-
35
- // ── Playbook fetching ────────────────────────────────────────────────
36
-
37
- interface PlaybookMatch {
38
- id: string;
39
- playbook: Playbook;
40
- }
41
-
42
- /**
43
- * Fetch all active playbooks that could apply to this message's channel,
44
- * returning both the parsed Playbook and the memory item ID.
45
- */
46
- function fetchMatchingPlaybooks(
47
- channel: string,
48
- scopeId = "default",
49
- ): PlaybookMatch[] {
50
- const db = getDb();
51
-
52
- const rows = db
53
- .select({
54
- id: memoryItems.id,
55
- statement: memoryItems.statement,
56
- })
57
- .from(memoryItems)
58
- .where(
59
- and(
60
- eq(memoryItems.kind, "playbook"),
61
- eq(memoryItems.status, "active"),
62
- eq(memoryItems.scopeId, scopeId),
63
- isNull(memoryItems.invalidAt),
64
- ),
65
- )
66
- .orderBy(desc(memoryItems.importance))
67
- .all();
68
-
69
- const matches: PlaybookMatch[] = [];
70
- for (const row of rows) {
71
- const playbook = parsePlaybookStatement(row.statement);
72
- if (!playbook) continue;
73
-
74
- // Include if the playbook applies to all channels or matches this channel
75
- if (playbook.channel === "*" || playbook.channel === channel) {
76
- matches.push({ id: row.id, playbook });
77
- }
78
- }
79
-
80
- return matches;
81
- }
82
-
83
- // ── LLM classification ──────────────────────────────────────────────
84
-
85
- function buildSystemPrompt(
86
- contact: ContactWithChannels | null,
87
- playbookMatches: PlaybookMatch[],
88
- ): string {
89
- const sections: string[] = [
90
- `You are a message triage system. Classify the incoming message into a category and suggest an action.`,
91
- ``,
92
- `Available categories (you may also use a custom category if none fit):`,
93
- ...DEFAULT_TRIAGE_CATEGORIES.map((c) => `- ${c}`),
94
- ];
95
-
96
- if (contact) {
97
- sections.push(``, `<sender-context>`, `Name: ${contact.displayName}`);
98
- if (contact.relationship)
99
- sections.push(`Relationship: ${contact.relationship}`);
100
- if (contact.importance !== 0.5)
101
- sections.push(`Importance: ${contact.importance}`);
102
- if (contact.responseExpectation)
103
- sections.push(`Response expectation: ${contact.responseExpectation}`);
104
- if (contact.preferredTone)
105
- sections.push(`Preferred tone: ${contact.preferredTone}`);
106
- sections.push(
107
- `Interaction count: ${contact.interactionCount}`,
108
- `</sender-context>`,
109
- );
110
- } else {
111
- sections.push(``, `Sender is not in the contact graph (unknown sender).`);
112
- }
113
-
114
- if (playbookMatches.length > 0) {
115
- sections.push(``, `<action-playbooks>`);
116
- for (const { playbook } of playbookMatches) {
117
- const channelLabel =
118
- playbook.channel === "*" ? "all channels" : playbook.channel;
119
- const autonomyLabel =
120
- playbook.autonomyLevel === "auto"
121
- ? "execute automatically"
122
- : playbook.autonomyLevel === "draft"
123
- ? "draft for review"
124
- : "notify only";
125
- sections.push(
126
- `- WHEN "${playbook.trigger}" on ${channelLabel} → ${playbook.action} [${autonomyLabel}, priority=${playbook.priority}]`,
127
- );
128
- }
129
- sections.push(`</action-playbooks>`);
130
- }
131
-
132
- sections.push(
133
- ``,
134
- `You MUST respond using the \`store_triage_result\` tool. Do not respond with text.`,
135
- );
136
-
137
- return sections.join("\n");
138
- }
139
-
140
- const STORE_TRIAGE_TOOL = {
141
- name: "store_triage_result",
142
- description: "Store the triage classification result for this message",
143
- input_schema: {
144
- type: "object" as const,
145
- properties: {
146
- category: {
147
- type: "string",
148
- description:
149
- "The triage category (e.g. needs_response, fyi, newsletter, cold_outreach, transactional, urgent, scheduling, or a custom category)",
150
- },
151
- confidence: {
152
- type: "number",
153
- description: "Confidence score between 0 and 1",
154
- },
155
- suggestedAction: {
156
- type: "string",
157
- description: "A concise description of the recommended action to take",
158
- },
159
- matchedPlaybookTriggers: {
160
- type: "array",
161
- items: { type: "string" },
162
- description:
163
- "List of playbook trigger strings that matched this message (empty array if none)",
164
- },
165
- },
166
- required: [
167
- "category",
168
- "confidence",
169
- "suggestedAction",
170
- "matchedPlaybookTriggers",
171
- ],
172
- },
173
- };
174
-
175
- function buildUserPrompt(message: InboundMessage): string {
176
- const parts: string[] = [
177
- `Channel: ${message.channel}`,
178
- `From: ${message.sender}`,
179
- ];
180
- if (message.subject) parts.push(`Subject: ${message.subject}`);
181
- if (message.threadId) parts.push(`Thread ID: ${message.threadId}`);
182
- parts.push(``, `Body:`, message.body);
183
-
184
- return `Classify this inbound message:\n\n${parts.join("\n")}`;
185
- }
186
-
187
- // ── Fallback classification ─────────────────────────────────────────
188
-
189
- function buildFallbackResult(): TriageResult {
190
- return {
191
- category: "needs_response",
192
- confidence: 0.3,
193
- suggestedAction: "Review manually — LLM classification unavailable",
194
- matchedPlaybooks: [],
195
- };
196
- }
197
-
198
- // ── Core triage function ────────────────────────────────────────────
199
-
200
- export async function triageMessage(
201
- message: InboundMessage,
202
- scopeId?: string,
203
- ): Promise<TriageResult> {
204
- // Step 1: Look up sender in contact graph
205
- const contact = findContactByAddress(message.channel, message.sender);
206
-
207
- // Step 2: Fetch matching playbooks
208
- const playbookMatches = fetchMatchingPlaybooks(message.channel, scopeId);
209
-
210
- // Step 3: Classify with LLM
211
- const provider = getConfiguredProvider();
212
- if (!provider) {
213
- log.warn(
214
- "Configured provider unavailable for triage classification, returning fallback",
215
- );
216
- const result = buildFallbackResult();
217
- persistTriageResult(message, result, playbookMatches);
218
- return result;
219
- }
220
-
221
- let result: TriageResult;
222
- try {
223
- result = await classifyWithLLM(message, contact, playbookMatches);
224
- } catch (err) {
225
- const errMsg = err instanceof Error ? err.message : String(err);
226
- log.warn({ err: errMsg }, "Triage LLM call failed, returning fallback");
227
- result = buildFallbackResult();
228
- }
229
-
230
- // Step 4: Persist the result
231
- persistTriageResult(message, result, playbookMatches);
232
-
233
- return result;
234
- }
235
-
236
- async function classifyWithLLM(
237
- message: InboundMessage,
238
- contact: ContactWithChannels | null,
239
- playbookMatches: PlaybookMatch[],
240
- ): Promise<TriageResult> {
241
- const provider = getConfiguredProvider()!;
242
- const { signal, cleanup } = createTimeout(TRIAGE_CLASSIFICATION_TIMEOUT_MS);
243
-
244
- const systemPrompt = buildSystemPrompt(contact, playbookMatches);
245
- const userPrompt = buildUserPrompt(message);
246
-
247
- try {
248
- const response = await provider.sendMessage(
249
- [userMessage(userPrompt)],
250
- [STORE_TRIAGE_TOOL],
251
- systemPrompt,
252
- {
253
- config: {
254
- modelIntent: TRIAGE_MODEL_INTENT,
255
- max_tokens: 1024,
256
- tool_choice: { type: "tool" as const, name: "store_triage_result" },
257
- },
258
- signal,
259
- },
260
- );
261
- cleanup();
262
-
263
- const toolBlock = extractToolUse(response);
264
- if (!toolBlock) {
265
- log.warn("No tool_use block in triage response, returning fallback");
266
- return buildFallbackResult();
267
- }
268
-
269
- const input = toolBlock.input as {
270
- category?: string;
271
- confidence?: number;
272
- suggestedAction?: string;
273
- matchedPlaybookTriggers?: string[];
274
- };
275
-
276
- const matchedTriggers = new Set(
277
- Array.isArray(input.matchedPlaybookTriggers)
278
- ? input.matchedPlaybookTriggers
279
- : [],
280
- );
281
-
282
- // Map LLM-identified triggers back to the full playbook data
283
- const matchedPlaybooks = playbookMatches
284
- .filter(({ playbook }) => matchedTriggers.has(playbook.trigger))
285
- .map(({ playbook }) => ({
286
- trigger: playbook.trigger,
287
- action: playbook.action,
288
- autonomyLevel: playbook.autonomyLevel,
289
- }));
290
-
291
- const confidence =
292
- typeof input.confidence === "number"
293
- ? Math.max(0, Math.min(1, input.confidence))
294
- : 0.5;
295
-
296
- return {
297
- category:
298
- typeof input.category === "string" ? input.category : "needs_response",
299
- confidence,
300
- suggestedAction:
301
- typeof input.suggestedAction === "string"
302
- ? truncate(input.suggestedAction, 500, "")
303
- : "Review manually",
304
- matchedPlaybooks,
305
- };
306
- } finally {
307
- cleanup();
308
- }
309
- }
310
-
311
- // ── Persistence ─────────────────────────────────────────────────────
312
-
313
- function persistTriageResult(
314
- message: InboundMessage,
315
- result: TriageResult,
316
- playbookMatches: PlaybookMatch[],
317
- ): void {
318
- try {
319
- const db = getDb();
320
- const matchedIds = playbookMatches
321
- .filter(({ playbook }) =>
322
- result.matchedPlaybooks.some((mp) => mp.trigger === playbook.trigger),
323
- )
324
- .map(({ id }) => id);
325
-
326
- db.insert(triageResults)
327
- .values({
328
- id: uuid(),
329
- channel: message.channel,
330
- sender: message.sender,
331
- category: result.category,
332
- confidence: result.confidence,
333
- suggestedAction: result.suggestedAction,
334
- matchedPlaybookIds:
335
- matchedIds.length > 0 ? JSON.stringify(matchedIds) : null,
336
- messageId: (message.metadata?.messageId as string) ?? null,
337
- createdAt: Date.now(),
338
- })
339
- .run();
340
- } catch (err) {
341
- const errMsg = err instanceof Error ? err.message : String(err);
342
- log.warn({ err: errMsg }, "Failed to persist triage result");
343
- }
344
- }