@vellumai/assistant 0.3.26 → 0.3.28

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 (82) hide show
  1. package/ARCHITECTURE.md +48 -1
  2. package/Dockerfile +2 -2
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +6 -2
  5. package/src/__tests__/agent-loop.test.ts +119 -0
  6. package/src/__tests__/bundled-asset.test.ts +107 -0
  7. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  8. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  9. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  10. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  11. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  12. package/src/__tests__/guardian-dispatch.test.ts +19 -19
  13. package/src/__tests__/guardian-routing-invariants.test.ts +954 -0
  14. package/src/__tests__/mcp-cli.test.ts +77 -0
  15. package/src/__tests__/non-member-access-request.test.ts +31 -29
  16. package/src/__tests__/notification-decision-fallback.test.ts +61 -3
  17. package/src/__tests__/notification-decision-strategy.test.ts +17 -0
  18. package/src/__tests__/notification-guardian-path.test.ts +13 -15
  19. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  20. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  21. package/src/__tests__/secret-scanner.test.ts +8 -0
  22. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  23. package/src/__tests__/session-runtime-assembly.test.ts +76 -47
  24. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  25. package/src/agent/loop.ts +46 -3
  26. package/src/approvals/guardian-decision-primitive.ts +285 -0
  27. package/src/approvals/guardian-request-resolvers.ts +539 -0
  28. package/src/calls/guardian-dispatch.ts +46 -40
  29. package/src/calls/relay-server.ts +147 -2
  30. package/src/calls/types.ts +1 -1
  31. package/src/config/system-prompt.ts +2 -1
  32. package/src/config/templates/BOOTSTRAP.md +47 -31
  33. package/src/config/templates/USER.md +5 -0
  34. package/src/config/update-bulletin-template-path.ts +4 -1
  35. package/src/config/vellum-skills/trusted-contacts/SKILL.md +22 -17
  36. package/src/daemon/handlers/guardian-actions.ts +45 -66
  37. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  38. package/src/daemon/lifecycle.ts +3 -16
  39. package/src/daemon/server.ts +18 -0
  40. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  41. package/src/daemon/session-agent-loop.ts +32 -5
  42. package/src/daemon/session-process.ts +68 -307
  43. package/src/daemon/session-runtime-assembly.ts +112 -24
  44. package/src/daemon/session-tool-setup.ts +1 -0
  45. package/src/daemon/session.ts +1 -0
  46. package/src/home-base/prebuilt/seed.ts +2 -1
  47. package/src/hooks/templates.ts +2 -1
  48. package/src/memory/canonical-guardian-store.ts +524 -0
  49. package/src/memory/channel-guardian-store.ts +1 -0
  50. package/src/memory/db-init.ts +16 -0
  51. package/src/memory/guardian-action-store.ts +7 -60
  52. package/src/memory/guardian-approvals.ts +9 -4
  53. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  54. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  55. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  56. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  57. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  58. package/src/memory/migrations/index.ts +4 -0
  59. package/src/memory/migrations/registry.ts +5 -0
  60. package/src/memory/schema-migration.ts +1 -0
  61. package/src/memory/schema.ts +52 -0
  62. package/src/notifications/copy-composer.ts +16 -4
  63. package/src/notifications/decision-engine.ts +57 -0
  64. package/src/permissions/defaults.ts +2 -0
  65. package/src/runtime/access-request-helper.ts +137 -0
  66. package/src/runtime/actor-trust-resolver.ts +225 -0
  67. package/src/runtime/channel-guardian-service.ts +12 -4
  68. package/src/runtime/guardian-context-resolver.ts +32 -7
  69. package/src/runtime/guardian-decision-types.ts +6 -0
  70. package/src/runtime/guardian-reply-router.ts +687 -0
  71. package/src/runtime/http-server.ts +8 -0
  72. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  73. package/src/runtime/routes/conversation-routes.ts +18 -0
  74. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  75. package/src/runtime/routes/inbound-message-handler.ts +170 -525
  76. package/src/runtime/tool-grant-request-helper.ts +195 -0
  77. package/src/tools/executor.ts +13 -1
  78. package/src/tools/sensitive-output-placeholders.ts +203 -0
  79. package/src/tools/tool-approval-handler.ts +44 -1
  80. package/src/tools/types.ts +11 -0
  81. package/src/util/bundled-asset.ts +31 -0
  82. package/src/util/canonicalize-identity.ts +52 -0
@@ -1,9 +1,8 @@
1
- import { applyGuardianDecision } from '../../approvals/guardian-decision-primitive.js';
2
- import { getPendingApprovalForRequest } from '../../memory/channel-guardian-store.js';
1
+ import {
2
+ applyCanonicalGuardianDecision,
3
+ } from '../../approvals/guardian-decision-primitive.js';
4
+ import { getCanonicalGuardianRequest } from '../../memory/canonical-guardian-store.js';
3
5
  import type { ApprovalAction } from '../../runtime/channel-approval-types.js';
4
- import { handleChannelDecision } from '../../runtime/channel-approvals.js';
5
- import * as pendingInteractions from '../../runtime/pending-interactions.js';
6
- import { handleAccessRequestDecision } from '../../runtime/routes/access-request-decision.js';
7
6
  import { listGuardianDecisionPrompts } from '../../runtime/routes/guardian-action-routes.js';
8
7
  import type { GuardianActionDecision, GuardianActionsPendingRequest } from '../ipc-protocol.js';
9
8
  import { defineHandlers, log } from './shared.js';
@@ -16,7 +15,8 @@ export const guardianActionsHandlers = defineHandlers({
16
15
  ctx.send(socket, { type: 'guardian_actions_pending_response', conversationId: msg.conversationId, prompts });
17
16
  },
18
17
 
19
- guardian_action_decision: (msg: GuardianActionDecision, socket, ctx) => {
18
+ guardian_action_decision: async (msg: GuardianActionDecision, socket, ctx) => {
19
+ try {
20
20
  // Validate the action is one of the known actions
21
21
  if (!VALID_ACTIONS.has(msg.action)) {
22
22
  log.warn({ requestId: msg.requestId, action: msg.action }, 'Invalid guardian action');
@@ -29,92 +29,71 @@ export const guardianActionsHandlers = defineHandlers({
29
29
  return;
30
30
  }
31
31
 
32
- // Try the channel guardian approval store first (tool approval prompts)
33
- const approval = getPendingApprovalForRequest(msg.requestId);
34
- if (approval) {
35
- // Enforce conversationId scoping when provided.
36
- if (msg.conversationId && msg.conversationId !== approval.conversationId) {
37
- log.warn({ requestId: msg.requestId, expected: approval.conversationId, got: msg.conversationId }, 'conversationId mismatch');
32
+ // Verify conversationId scoping before applying the canonical decision.
33
+ // A caller must not be able to cross-resolve requests from a different conversation.
34
+ if (msg.conversationId) {
35
+ const canonicalRequest = getCanonicalGuardianRequest(msg.requestId);
36
+ if (canonicalRequest && canonicalRequest.conversationId && canonicalRequest.conversationId !== msg.conversationId) {
37
+ log.warn({ requestId: msg.requestId, expected: canonicalRequest.conversationId, got: msg.conversationId }, 'conversationId mismatch');
38
38
  ctx.send(socket, {
39
39
  type: 'guardian_action_decision_response',
40
40
  applied: false,
41
- reason: 'conversation_mismatch',
41
+ reason: 'not_found',
42
42
  requestId: msg.requestId,
43
43
  });
44
44
  return;
45
45
  }
46
-
47
- // Access request approvals need a separate decision path — they don't have
48
- // pending interactions and use verification sessions instead.
49
- if (approval.toolName === 'ingress_access_request') {
50
- const mappedAction = msg.action === 'reject' ? 'deny' as const : 'approve' as const;
51
- // Use 'desktop' as the actor identity because this endpoint is
52
- // unauthenticated — we cannot verify the caller is the assigned
53
- // guardian, so we record a generic desktop origin instead of
54
- // falsely attributing the decision to guardianExternalUserId.
55
- const decisionResult = handleAccessRequestDecision(
56
- approval,
57
- mappedAction,
58
- 'desktop',
59
- );
60
- ctx.send(socket, {
61
- type: 'guardian_action_decision_response',
62
- applied: decisionResult.type !== 'stale',
63
- requestId: msg.requestId,
64
- reason: decisionResult.type === 'stale' ? 'stale' : undefined,
65
- });
66
- return;
67
- }
68
-
69
- const result = applyGuardianDecision({
70
- approval,
71
- decision: { action: msg.action as 'approve_once' | 'approve_always' | 'reject', source: 'plain_text', requestId: msg.requestId },
72
- actorExternalUserId: undefined,
73
- actorChannel: 'vellum',
74
- });
75
- ctx.send(socket, {
76
- type: 'guardian_action_decision_response',
77
- applied: result.applied,
78
- reason: result.reason,
79
- requestId: result.requestId ?? msg.requestId,
80
- });
81
- return;
82
46
  }
83
47
 
84
- // Fall back to the pending interactions tracker (direct confirmation requests).
85
- // Route through handleChannelDecision so approve_always properly persists trust rules.
86
- const interaction = pendingInteractions.get(msg.requestId);
87
- if (interaction) {
88
- // Enforce conversationId scoping when provided.
89
- if (msg.conversationId && msg.conversationId !== interaction.conversationId) {
90
- log.warn({ requestId: msg.requestId, expected: interaction.conversationId, got: msg.conversationId }, 'conversationId mismatch');
48
+ const canonicalResult = await applyCanonicalGuardianDecision({
49
+ requestId: msg.requestId,
50
+ action: msg.action as ApprovalAction,
51
+ actorContext: {
52
+ externalUserId: undefined,
53
+ channel: 'vellum',
54
+ isTrusted: true,
55
+ },
56
+ userText: undefined,
57
+ });
58
+
59
+ if (canonicalResult.applied) {
60
+ // When the CAS committed but the resolver failed, the side effect
61
+ // (e.g. minting a verification session) did not happen. From the
62
+ // caller's perspective the decision was not truly applied.
63
+ if (canonicalResult.resolverFailed) {
91
64
  ctx.send(socket, {
92
65
  type: 'guardian_action_decision_response',
93
66
  applied: false,
94
- reason: 'conversation_mismatch',
95
- requestId: msg.requestId,
67
+ reason: 'resolver_failed',
68
+ resolverFailureReason: canonicalResult.resolverFailureReason,
69
+ requestId: canonicalResult.requestId,
96
70
  });
97
71
  return;
98
72
  }
99
73
 
100
- const result = handleChannelDecision(
101
- interaction.conversationId,
102
- { action: msg.action as ApprovalAction, source: 'plain_text', requestId: msg.requestId },
103
- );
104
74
  ctx.send(socket, {
105
75
  type: 'guardian_action_decision_response',
106
- applied: result.applied,
107
- requestId: result.requestId ?? msg.requestId,
76
+ applied: true,
77
+ requestId: canonicalResult.requestId,
108
78
  });
109
79
  return;
110
80
  }
111
81
 
112
- log.warn({ requestId: msg.requestId }, 'No pending guardian action found for requestId');
82
+ // Return the reason for failure (stale, expired, not_found, etc.)
113
83
  ctx.send(socket, {
114
84
  type: 'guardian_action_decision_response',
115
85
  applied: false,
116
- reason: 'not_found',
86
+ reason: canonicalResult.reason,
117
87
  requestId: msg.requestId,
118
88
  });
89
+ } catch (err) {
90
+ log.error({ err, requestId: msg.requestId }, 'guardian_action_decision: unhandled error');
91
+ ctx.send(socket, {
92
+ type: 'guardian_action_decision_response',
93
+ applied: false,
94
+ reason: 'internal_error',
95
+ requestId: msg.requestId,
96
+ });
97
+ }
119
98
  },
120
99
  });
@@ -31,6 +31,12 @@ export interface GuardianActionsPendingResponse {
31
31
  expiresAt: number;
32
32
  conversationId: string;
33
33
  callSessionId: string | null;
34
+ /**
35
+ * Canonical request kind (e.g. 'tool_approval', 'pending_question').
36
+ * Present when the prompt originates from the canonical guardian request
37
+ * store. Absent for legacy-only prompts.
38
+ */
39
+ kind?: string;
34
40
  }>;
35
41
  }
36
42
 
@@ -38,6 +44,7 @@ export interface GuardianActionDecisionResponse {
38
44
  type: 'guardian_action_decision_response';
39
45
  applied: boolean;
40
46
  reason?: string;
47
+ resolverFailureReason?: string;
41
48
  requestId?: string;
42
49
  userText?: string;
43
50
  }
@@ -59,7 +59,6 @@ import type { ServerMessage } from './ipc-protocol.js';
59
59
  import { initializeProvidersAndTools, registerMessagingProviders,registerWatcherProviders } from './providers-setup.js';
60
60
  import { seedInterfaceFiles } from './seed-files.js';
61
61
  import { DaemonServer } from './server.js';
62
- import { setApprovalConversationGenerator, setGuardianActionCopyGenerator, setGuardianFollowUpConversationGenerator } from './session-process.js';
63
62
  import { initSlashPairingContext } from './session-slash.js';
64
63
  import { installShutdownHandlers } from './shutdown-handlers.js';
65
64
 
@@ -320,21 +319,9 @@ export async function runDaemon(): Promise<void> {
320
319
  server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel, sourceInterface),
321
320
  interfacesDir: getInterfacesDir(),
322
321
  approvalCopyGenerator: createApprovalCopyGenerator(),
323
- approvalConversationGenerator: (() => {
324
- const gen = createApprovalConversationGenerator();
325
- setApprovalConversationGenerator(gen);
326
- return gen;
327
- })(),
328
- guardianActionCopyGenerator: (() => {
329
- const gen = createGuardianActionCopyGenerator();
330
- setGuardianActionCopyGenerator(gen);
331
- return gen;
332
- })(),
333
- guardianFollowUpConversationGenerator: (() => {
334
- const gen = createGuardianFollowUpConversationGenerator();
335
- setGuardianFollowUpConversationGenerator(gen);
336
- return gen;
337
- })(),
322
+ approvalConversationGenerator: createApprovalConversationGenerator(),
323
+ guardianActionCopyGenerator: createGuardianActionCopyGenerator(),
324
+ guardianFollowUpConversationGenerator: createGuardianFollowUpConversationGenerator(),
338
325
  sendMessageDeps: {
339
326
  getOrCreateSession: (conversationId) =>
340
327
  server.getSessionForMessages(conversationId),
@@ -10,6 +10,10 @@ import { buildSystemPrompt } from '../config/system-prompt.js';
10
10
  import type { HeartbeatService } from '../heartbeat/heartbeat-service.js';
11
11
  import { bootstrapHomeBaseAppLink } from '../home-base/bootstrap.js';
12
12
  import * as attachmentsStore from '../memory/attachments-store.js';
13
+ import {
14
+ createCanonicalGuardianRequest,
15
+ generateCanonicalRequestCode,
16
+ } from '../memory/canonical-guardian-store.js';
13
17
  import * as conversationStore from '../memory/conversation-store.js';
14
18
  import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
15
19
  import { RateLimitProvider } from '../providers/ratelimit.js';
@@ -114,6 +118,20 @@ function makePendingInteractionRegistrar(
114
118
  persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
115
119
  },
116
120
  });
121
+
122
+ // Create a canonical guardian request so IPC/HTTP handlers can find it
123
+ // via applyCanonicalGuardianDecision.
124
+ createCanonicalGuardianRequest({
125
+ id: msg.requestId,
126
+ kind: 'tool_approval',
127
+ sourceType: 'desktop',
128
+ sourceChannel: 'vellum',
129
+ conversationId,
130
+ toolName: msg.toolName,
131
+ status: 'pending',
132
+ requestCode: generateCanonicalRequestCode(),
133
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
134
+ });
117
135
  } else if (msg.type === 'secret_request') {
118
136
  pendingInteractions.register(msg.requestId, {
119
137
  session,
@@ -330,6 +330,7 @@ export async function handleMessageComplete(
330
330
  // Clean assistant content and accumulate directives
331
331
  const { cleanedContent, directives: msgDirectives, warnings: msgWarnings } =
332
332
  cleanAssistantContent(event.message.content);
333
+ const cleanedBlocks = cleanedContent as ContentBlock[];
333
334
  state.accumulatedDirectives.push(...msgDirectives);
334
335
  state.directiveWarnings.push(...msgWarnings);
335
336
  if (msgDirectives.length > 0) {
@@ -340,7 +341,7 @@ export async function handleMessageComplete(
340
341
  }
341
342
 
342
343
  // Build content with UI surfaces
343
- const contentWithSurfaces: ContentBlock[] = [...cleanedContent as ContentBlock[]];
344
+ const contentWithSurfaces: ContentBlock[] = [...cleanedBlocks];
344
345
  for (const surface of deps.ctx.currentTurnSurfaces) {
345
346
  contentWithSurfaces.push({
346
347
  type: 'ui_surface',
@@ -371,9 +372,9 @@ export async function handleMessageComplete(
371
372
  deps.ctx.currentTurnSurfaces = [];
372
373
 
373
374
  // Emit trace event
374
- const charCount = cleanedContent
375
- .filter((b) => (b as Record<string, unknown>).type === 'text')
376
- .reduce((sum: number, b) => sum + ((b as { text?: string }).text?.length ?? 0), 0);
375
+ const charCount = cleanedBlocks
376
+ .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
377
+ .reduce((sum, b) => sum + b.text.length, 0);
377
378
  const toolUseCount = event.message.content
378
379
  .filter((b) => b.type === 'tool_use')
379
380
  .length;
@@ -25,6 +25,7 @@ import { stripMemoryRecallMessages } from '../memory/retriever.js';
25
25
  import type { PermissionPrompter } from '../permissions/prompter.js';
26
26
  import type { ContentBlock,Message } from '../providers/types.js';
27
27
  import type { Provider } from '../providers/types.js';
28
+ import { resolveActorTrust } from '../runtime/actor-trust-resolver.js';
28
29
  import type { UsageActor } from '../usage/actors.js';
29
30
  import { getLogger } from '../util/logger.js';
30
31
  import { truncate } from '../util/truncate.js';
@@ -55,9 +56,11 @@ import { raceWithTimeout,stripMediaPayloadsForRetry } from './session-media-retr
55
56
  import { prepareMemoryContext } from './session-memory.js';
56
57
  import type { MessageQueue } from './session-queue-manager.js';
57
58
  import type { QueueDrainReason } from './session-queue-manager.js';
58
- import type { ActiveSurfaceContext, ChannelCapabilities, ChannelTurnContextParams, GuardianRuntimeContext,InterfaceTurnContextParams } from './session-runtime-assembly.js';
59
+ import type { ActiveSurfaceContext, ChannelCapabilities, ChannelTurnContextParams, GuardianRuntimeContext, InboundActorContext, InterfaceTurnContextParams } from './session-runtime-assembly.js';
59
60
  import {
60
61
  applyRuntimeInjections,
62
+ inboundActorContextFromGuardian,
63
+ inboundActorContextFromTrust,
61
64
  stripInjectedContext,
62
65
  } from './session-runtime-assembly.js';
63
66
  import type { SkillProjectionCache } from './session-skill-tools.js';
@@ -102,6 +105,7 @@ export interface AgentLoopSessionContext {
102
105
  channelCapabilities?: ChannelCapabilities;
103
106
  commandIntent?: { type: string; payload?: string; languageCode?: string };
104
107
  guardianContext?: GuardianRuntimeContext;
108
+ assistantId?: string;
105
109
  voiceCallControlPrompt?: string;
106
110
 
107
111
  readonly coreToolNames: Set<string>;
@@ -349,6 +353,28 @@ export async function runAgentLoopImpl(
349
353
  conversationOriginInterface: getConversationOriginInterface(ctx.conversationId),
350
354
  };
351
355
 
356
+ // Resolve the inbound actor context for the model's <inbound_actor_context>
357
+ // block. When the session carries enough identity info, use the unified
358
+ // actor trust resolver so trusted_contact classifications propagate
359
+ // correctly (the legacy guardian-context path collapses non-guardian to
360
+ // 'unknown'). The guardian context is still used for policy gating — only
361
+ // the model context block uses the trust-resolved output.
362
+ let resolvedInboundActorContext: InboundActorContext | null = null;
363
+ if (ctx.guardianContext) {
364
+ const gc = ctx.guardianContext;
365
+ if (gc.requesterExternalUserId && gc.requesterChatId) {
366
+ const actorTrust = resolveActorTrust({
367
+ assistantId: ctx.assistantId ?? 'self',
368
+ sourceChannel: gc.sourceChannel,
369
+ externalChatId: gc.requesterChatId,
370
+ senderExternalUserId: gc.requesterExternalUserId,
371
+ });
372
+ resolvedInboundActorContext = inboundActorContextFromTrust(actorTrust);
373
+ } else {
374
+ resolvedInboundActorContext = inboundActorContextFromGuardian(gc);
375
+ }
376
+ }
377
+
352
378
  const isInteractiveResolved = options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock);
353
379
  runMessages = applyRuntimeInjections(runMessages, {
354
380
  softConflictInstruction,
@@ -358,7 +384,7 @@ export async function runAgentLoopImpl(
358
384
  channelCommandContext: ctx.commandIntent ?? null,
359
385
  channelTurnContext,
360
386
  interfaceTurnContext,
361
- guardianContext: ctx.guardianContext ?? null,
387
+ inboundActorContext: resolvedInboundActorContext,
362
388
  temporalContext,
363
389
  voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
364
390
  isNonInteractive: !isInteractiveResolved,
@@ -477,7 +503,7 @@ export async function runAgentLoopImpl(
477
503
  channelCommandContext: ctx.commandIntent ?? null,
478
504
  channelTurnContext,
479
505
  interfaceTurnContext,
480
- guardianContext: ctx.guardianContext ?? null,
506
+ inboundActorContext: resolvedInboundActorContext,
481
507
  temporalContext,
482
508
  voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
483
509
  isNonInteractive: !isInteractiveResolved,
@@ -515,7 +541,7 @@ export async function runAgentLoopImpl(
515
541
  channelCommandContext: ctx.commandIntent ?? null,
516
542
  channelTurnContext,
517
543
  interfaceTurnContext,
518
- guardianContext: ctx.guardianContext ?? null,
544
+ inboundActorContext: resolvedInboundActorContext,
519
545
  temporalContext,
520
546
  voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
521
547
  isNonInteractive: !isInteractiveResolved,
@@ -604,7 +630,8 @@ export async function runAgentLoopImpl(
604
630
  const newMessages = updatedHistory.slice(preRunHistoryLength).map((msg) => {
605
631
  if (msg.role !== 'assistant') return msg;
606
632
  const { cleanedContent } = cleanAssistantContent(msg.content);
607
- return { ...msg, content: cleanedContent as ContentBlock[] };
633
+ const cleanedBlocks = cleanedContent as ContentBlock[];
634
+ return { ...msg, content: cleanedBlocks };
608
635
  });
609
636
 
610
637
  const hasAssistantResponse = newMessages.some((msg) => msg.role === 'assistant');