@vellumai/assistant 0.4.4 → 0.4.6

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 (90) hide show
  1. package/ARCHITECTURE.md +4 -4
  2. package/README.md +6 -6
  3. package/bun.lock +6 -2
  4. package/docs/architecture/memory.md +4 -4
  5. package/package.json +2 -2
  6. package/src/__tests__/actor-token-service.test.ts +5 -2
  7. package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
  8. package/src/__tests__/call-controller.test.ts +78 -0
  9. package/src/__tests__/call-domain.test.ts +148 -10
  10. package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
  11. package/src/__tests__/call-pointer-messages.test.ts +105 -43
  12. package/src/__tests__/canonical-guardian-store.test.ts +44 -10
  13. package/src/__tests__/channel-approval-routes.test.ts +67 -65
  14. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
  15. package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
  16. package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
  17. package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
  18. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
  19. package/src/__tests__/guardian-grant-minting.test.ts +24 -24
  20. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
  21. package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
  22. package/src/__tests__/guardian-routing-state.test.ts +4 -4
  23. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
  24. package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
  25. package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
  26. package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
  27. package/src/__tests__/non-member-access-request.test.ts +50 -47
  28. package/src/__tests__/relay-server.test.ts +71 -0
  29. package/src/__tests__/send-endpoint-busy.test.ts +6 -0
  30. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
  31. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
  32. package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
  33. package/src/__tests__/system-prompt.test.ts +1 -0
  34. package/src/__tests__/tool-approval-handler.test.ts +1 -1
  35. package/src/__tests__/tool-grant-request-escalation.test.ts +9 -2
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
  39. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  40. package/src/approvals/guardian-decision-primitive.ts +29 -25
  41. package/src/approvals/guardian-request-resolvers.ts +9 -5
  42. package/src/calls/call-pointer-message-composer.ts +27 -85
  43. package/src/calls/call-pointer-messages.ts +54 -21
  44. package/src/calls/guardian-dispatch.ts +30 -0
  45. package/src/calls/relay-server.ts +13 -13
  46. package/src/config/system-prompt.ts +10 -3
  47. package/src/config/templates/BOOTSTRAP.md +6 -5
  48. package/src/config/templates/USER.md +1 -0
  49. package/src/config/user-reference.ts +44 -0
  50. package/src/daemon/handlers/guardian-actions.ts +5 -2
  51. package/src/daemon/handlers/sessions.ts +8 -3
  52. package/src/daemon/lifecycle.ts +109 -3
  53. package/src/daemon/server.ts +32 -24
  54. package/src/daemon/session-agent-loop.ts +4 -3
  55. package/src/daemon/session-lifecycle.ts +1 -9
  56. package/src/daemon/session-process.ts +2 -2
  57. package/src/daemon/session-runtime-assembly.ts +2 -0
  58. package/src/daemon/session-tool-setup.ts +10 -0
  59. package/src/daemon/session.ts +1 -0
  60. package/src/memory/canonical-guardian-store.ts +40 -0
  61. package/src/memory/conversation-crud.ts +26 -0
  62. package/src/memory/conversation-store.ts +1 -0
  63. package/src/memory/db-init.ts +8 -0
  64. package/src/memory/guardian-bindings.ts +4 -0
  65. package/src/memory/job-handlers/backfill.ts +2 -9
  66. package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
  67. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
  68. package/src/memory/migrations/index.ts +2 -0
  69. package/src/memory/migrations/registry.ts +5 -0
  70. package/src/memory/schema.ts +3 -0
  71. package/src/notifications/copy-composer.ts +2 -2
  72. package/src/runtime/access-request-helper.ts +43 -28
  73. package/src/runtime/actor-trust-resolver.ts +19 -14
  74. package/src/runtime/channel-guardian-service.ts +6 -0
  75. package/src/runtime/guardian-context-resolver.ts +6 -2
  76. package/src/runtime/guardian-reply-router.ts +33 -16
  77. package/src/runtime/guardian-vellum-migration.ts +29 -5
  78. package/src/runtime/http-types.ts +0 -13
  79. package/src/runtime/local-actor-identity.ts +19 -13
  80. package/src/runtime/middleware/actor-token.ts +2 -2
  81. package/src/runtime/routes/channel-delivery-routes.ts +5 -5
  82. package/src/runtime/routes/conversation-routes.ts +45 -35
  83. package/src/runtime/routes/guardian-action-routes.ts +7 -1
  84. package/src/runtime/routes/guardian-approval-interception.ts +52 -52
  85. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -0
  86. package/src/runtime/routes/inbound-conversation.ts +7 -7
  87. package/src/runtime/routes/inbound-message-handler.ts +105 -94
  88. package/src/runtime/tool-grant-request-helper.ts +1 -0
  89. package/src/util/logger.ts +10 -0
  90. package/src/daemon/call-pointer-generators.ts +0 -59
@@ -119,14 +119,14 @@ export async function handleChannelInbound(
119
119
  const body = await req.json() as {
120
120
  sourceChannel?: string;
121
121
  interface?: string;
122
- externalChatId?: string;
122
+ conversationExternalId?: string;
123
123
  externalMessageId?: string;
124
124
  content?: string;
125
125
  isEdit?: boolean;
126
- senderName?: string;
126
+ actorDisplayName?: string;
127
127
  attachmentIds?: string[];
128
- senderExternalUserId?: string;
129
- senderUsername?: string;
128
+ actorExternalId?: string;
129
+ actorUsername?: string;
130
130
  sourceMetadata?: Record<string, unknown>;
131
131
  replyCallbackUrl?: string;
132
132
  callbackQueryId?: string;
@@ -134,7 +134,7 @@ export async function handleChannelInbound(
134
134
  };
135
135
 
136
136
  const {
137
- externalChatId,
137
+ conversationExternalId,
138
138
  externalMessageId,
139
139
  content,
140
140
  isEdit,
@@ -161,8 +161,11 @@ export async function handleChannelInbound(
161
161
  return httpError('BAD_REQUEST', `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}`, 400);
162
162
  }
163
163
 
164
- if (!externalChatId || typeof externalChatId !== 'string') {
165
- return httpError('BAD_REQUEST', 'externalChatId is required', 400);
164
+ if (!conversationExternalId || typeof conversationExternalId !== 'string') {
165
+ return httpError('BAD_REQUEST', 'conversationExternalId is required', 400);
166
+ }
167
+ if (!body.actorExternalId || typeof body.actorExternalId !== 'string' || !body.actorExternalId.trim()) {
168
+ return httpError('BAD_REQUEST', 'actorExternalId is required', 400);
166
169
  }
167
170
  if (!externalMessageId || typeof externalMessageId !== 'string') {
168
171
  return httpError('BAD_REQUEST', 'externalMessageId is required', 400);
@@ -189,12 +192,12 @@ export async function handleChannelInbound(
189
192
  log.debug({ raw: assistantId, canonical: canonicalAssistantId }, 'Canonicalized channel assistant ID');
190
193
  }
191
194
 
192
- // Coerce senderExternalUserId to a string at the boundary — the field
195
+ // Coerce actorExternalId to a string at the boundary — the field
193
196
  // comes from unvalidated JSON and may be a number, object, or other
194
197
  // non-string type. Non-string truthy values would throw inside
195
198
  // canonicalizeInboundIdentity when it calls .trim().
196
- const rawSenderId = body.senderExternalUserId != null
197
- ? String(body.senderExternalUserId)
199
+ const rawSenderId = body.actorExternalId != null
200
+ ? String(body.actorExternalId)
198
201
  : undefined;
199
202
 
200
203
  // Canonicalize the sender identity so all trust lookups, member matching,
@@ -206,7 +209,7 @@ export async function handleChannelInbound(
206
209
  : null;
207
210
 
208
211
  // Track whether the original payload included a sender identity. A
209
- // whitespace-only senderExternalUserId canonicalizes to null but still
212
+ // whitespace-only actorExternalId canonicalizes to null but still
210
213
  // represents an explicit (malformed) identity claim that must enter the
211
214
  // ACL deny path rather than bypassing it.
212
215
  const hasSenderIdentityClaim = rawSenderId !== undefined;
@@ -253,7 +256,7 @@ export async function handleChannelInbound(
253
256
  assistantId: canonicalAssistantId,
254
257
  sourceChannel,
255
258
  externalUserId: canonicalSenderId,
256
- externalChatId,
259
+ externalChatId: conversationExternalId,
257
260
  });
258
261
  }
259
262
 
@@ -300,11 +303,11 @@ export async function handleChannelInbound(
300
303
  const inviteResult = await handleInviteTokenIntercept({
301
304
  rawToken: inviteToken,
302
305
  sourceChannel,
303
- externalChatId,
306
+ externalChatId: conversationExternalId,
304
307
  externalMessageId,
305
308
  senderExternalUserId: canonicalSenderId ?? rawSenderId,
306
- senderName: body.senderName,
307
- senderUsername: body.senderUsername,
309
+ senderName: body.actorDisplayName,
310
+ senderUsername: body.actorUsername,
308
311
  replyCallbackUrl: body.replyCallbackUrl,
309
312
  bearerToken,
310
313
  assistantId,
@@ -324,14 +327,14 @@ export async function handleChannelInbound(
324
327
  const accessResult = notifyGuardianOfAccessRequest({
325
328
  canonicalAssistantId,
326
329
  sourceChannel,
327
- externalChatId,
328
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
329
- senderName: body.senderName,
330
- senderUsername: body.senderUsername,
330
+ conversationExternalId,
331
+ actorExternalId: canonicalSenderId ?? rawSenderId,
332
+ actorDisplayName: body.actorDisplayName,
333
+ actorUsername: body.actorUsername,
331
334
  });
332
335
  guardianNotified = accessResult.notified;
333
336
  } catch (err) {
334
- log.error({ err, sourceChannel, externalChatId }, 'Failed to notify guardian of access request');
337
+ log.error({ err, sourceChannel, conversationExternalId }, 'Failed to notify guardian of access request');
335
338
  }
336
339
 
337
340
  if (body.replyCallbackUrl) {
@@ -340,12 +343,12 @@ export async function handleChannelInbound(
340
343
  : "Sorry, you haven't been approved to message this assistant.";
341
344
  try {
342
345
  await deliverChannelReply(body.replyCallbackUrl, {
343
- chatId: externalChatId,
346
+ chatId: conversationExternalId,
344
347
  text: replyText,
345
348
  assistantId,
346
349
  }, bearerToken);
347
350
  } catch (err) {
348
- log.error({ err, externalChatId }, 'Failed to deliver ACL rejection reply');
351
+ log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
349
352
  }
350
353
  }
351
354
 
@@ -386,11 +389,11 @@ export async function handleChannelInbound(
386
389
  const inviteResult = await handleInviteTokenIntercept({
387
390
  rawToken: inviteToken,
388
391
  sourceChannel,
389
- externalChatId,
392
+ externalChatId: conversationExternalId,
390
393
  externalMessageId,
391
394
  senderExternalUserId: canonicalSenderId ?? rawSenderId,
392
- senderName: body.senderName,
393
- senderUsername: body.senderUsername,
395
+ senderName: body.actorDisplayName,
396
+ senderUsername: body.actorUsername,
394
397
  replyCallbackUrl: body.replyCallbackUrl,
395
398
  bearerToken,
396
399
  assistantId,
@@ -411,15 +414,15 @@ export async function handleChannelInbound(
411
414
  const accessResult = notifyGuardianOfAccessRequest({
412
415
  canonicalAssistantId,
413
416
  sourceChannel,
414
- externalChatId,
415
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
416
- senderName: body.senderName,
417
- senderUsername: body.senderUsername,
417
+ conversationExternalId,
418
+ actorExternalId: canonicalSenderId ?? rawSenderId,
419
+ actorDisplayName: body.actorDisplayName,
420
+ actorUsername: body.actorUsername,
418
421
  previousMemberStatus: resolvedMember.status,
419
422
  });
420
423
  guardianNotified = accessResult.notified;
421
424
  } catch (err) {
422
- log.error({ err, sourceChannel, externalChatId }, 'Failed to notify guardian of access request');
425
+ log.error({ err, sourceChannel, conversationExternalId }, 'Failed to notify guardian of access request');
423
426
  }
424
427
  }
425
428
 
@@ -429,12 +432,12 @@ export async function handleChannelInbound(
429
432
  : "Sorry, you haven't been approved to message this assistant.";
430
433
  try {
431
434
  await deliverChannelReply(body.replyCallbackUrl, {
432
- chatId: externalChatId,
435
+ chatId: conversationExternalId,
433
436
  text: replyText,
434
437
  assistantId,
435
438
  }, bearerToken);
436
439
  } catch (err) {
437
- log.error({ err, externalChatId }, 'Failed to deliver ACL rejection reply');
440
+ log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
438
441
  }
439
442
  }
440
443
  return Response.json({ accepted: true, denied: true, reason: `member_${resolvedMember.status}` });
@@ -446,12 +449,12 @@ export async function handleChannelInbound(
446
449
  if (body.replyCallbackUrl) {
447
450
  try {
448
451
  await deliverChannelReply(body.replyCallbackUrl, {
449
- chatId: externalChatId,
452
+ chatId: conversationExternalId,
450
453
  text: "Sorry, you haven't been approved to message this assistant.",
451
454
  assistantId,
452
455
  }, bearerToken);
453
456
  } catch (err) {
454
- log.error({ err, externalChatId }, 'Failed to deliver ACL rejection reply');
457
+ log.error({ err, conversationExternalId }, 'Failed to deliver ACL rejection reply');
455
458
  }
456
459
  }
457
460
  return Response.json({ accepted: true, denied: true, reason: 'policy_deny' });
@@ -487,7 +490,7 @@ export async function handleChannelInbound(
487
490
  // Dedup the edit event itself (retried edited_message webhooks)
488
491
  const editResult = channelDeliveryStore.recordInbound(
489
492
  sourceChannel,
490
- externalChatId,
493
+ conversationExternalId,
491
494
  externalMessageId,
492
495
  { sourceMessageId, assistantId: canonicalAssistantId },
493
496
  );
@@ -510,7 +513,7 @@ export async function handleChannelInbound(
510
513
  for (let attempt = 0; attempt <= EDIT_LOOKUP_RETRIES; attempt++) {
511
514
  original = channelDeliveryStore.findMessageBySourceId(
512
515
  sourceChannel,
513
- externalChatId,
516
+ conversationExternalId,
514
517
  sourceMessageId,
515
518
  );
516
519
  if (original) break;
@@ -531,7 +534,7 @@ export async function handleChannelInbound(
531
534
  );
532
535
  } else {
533
536
  log.warn(
534
- { assistantId, sourceChannel, externalChatId, sourceMessageId },
537
+ { assistantId, sourceChannel, conversationExternalId, sourceMessageId },
535
538
  'Could not find original message for edit after retries, ignoring',
536
539
  );
537
540
  }
@@ -546,7 +549,7 @@ export async function handleChannelInbound(
546
549
  // ── New message path ──
547
550
  const result = channelDeliveryStore.recordInbound(
548
551
  sourceChannel,
549
- externalChatId,
552
+ conversationExternalId,
550
553
  externalMessageId,
551
554
  { sourceMessageId, assistantId: canonicalAssistantId },
552
555
  );
@@ -586,10 +589,10 @@ export async function handleChannelInbound(
586
589
  externalConversationStore.upsertBinding({
587
590
  conversationId: result.conversationId,
588
591
  sourceChannel,
589
- externalChatId,
592
+ externalChatId: conversationExternalId,
590
593
  externalUserId: canonicalSenderId ?? rawSenderId ?? null,
591
- displayName: body.senderName ?? null,
592
- username: body.senderUsername ?? null,
594
+ displayName: body.actorDisplayName ?? null,
595
+ username: body.actorUsername ?? null,
593
596
  });
594
597
  }
595
598
 
@@ -608,26 +611,34 @@ export async function handleChannelInbound(
608
611
  // Persist the raw payload so the decide handler can recover the original
609
612
  // message content when the escalation is approved.
610
613
  channelDeliveryStore.storePayload(result.eventId, {
611
- sourceChannel, interface: sourceInterface, externalChatId, externalMessageId, content,
614
+ sourceChannel, interface: sourceInterface, externalChatId: conversationExternalId, externalMessageId, content,
612
615
  attachmentIds, sourceMetadata: body.sourceMetadata,
613
- senderName: body.senderName,
614
- senderExternalUserId: body.senderExternalUserId,
615
- senderUsername: body.senderUsername,
616
+ senderName: body.actorDisplayName,
617
+ senderExternalUserId: body.actorExternalId,
618
+ senderUsername: body.actorUsername,
616
619
  replyCallbackUrl: body.replyCallbackUrl,
617
620
  assistantId: canonicalAssistantId,
618
621
  });
619
622
 
620
- createCanonicalGuardianRequest({
621
- kind: 'tool_approval',
622
- sourceType: 'channel',
623
- sourceChannel,
624
- conversationId: result.conversationId,
625
- requesterExternalUserId: canonicalSenderId ?? rawSenderId ?? undefined,
626
- guardianExternalUserId: binding.guardianExternalUserId,
627
- toolName: 'ingress_message',
628
- questionText: 'Ingress policy requires guardian approval',
629
- expiresAt: new Date(Date.now() + GUARDIAN_APPROVAL_TTL_MS).toISOString(),
630
- });
623
+ try {
624
+ createCanonicalGuardianRequest({
625
+ kind: 'tool_approval',
626
+ sourceType: 'channel',
627
+ sourceChannel,
628
+ conversationId: result.conversationId,
629
+ requesterExternalUserId: canonicalSenderId ?? rawSenderId ?? undefined,
630
+ guardianExternalUserId: binding.guardianExternalUserId,
631
+ guardianPrincipalId: binding.guardianPrincipalId ?? undefined,
632
+ toolName: 'ingress_message',
633
+ questionText: 'Ingress policy requires guardian approval',
634
+ expiresAt: new Date(Date.now() + GUARDIAN_APPROVAL_TTL_MS).toISOString(),
635
+ });
636
+ } catch (err) {
637
+ log.warn(
638
+ { err, conversationId: result.conversationId, sourceChannel },
639
+ 'Failed to create canonical guardian request for ingress escalation — escalation continues via notification pipeline',
640
+ );
641
+ }
631
642
 
632
643
  // Emit notification signal through the unified pipeline (fire-and-forget).
633
644
  // This lets the decision engine route escalation alerts to all configured
@@ -646,8 +657,8 @@ export async function handleChannelInbound(
646
657
  contextPayload: {
647
658
  conversationId: result.conversationId,
648
659
  sourceChannel,
649
- externalChatId,
650
- senderIdentifier: body.senderName || body.senderUsername || rawSenderId || 'Unknown sender',
660
+ conversationExternalId,
661
+ senderIdentifier: body.actorDisplayName || body.actorUsername || rawSenderId || 'Unknown sender',
651
662
  eventId: result.eventId,
652
663
  },
653
664
  dedupeKey: `escalation:${result.eventId}`,
@@ -700,7 +711,7 @@ export async function handleChannelInbound(
700
711
 
701
712
  if (bootstrapSession && bootstrapSession.status === 'pending_bootstrap') {
702
713
  // Bind the pending_bootstrap session to the sender's identity
703
- bindSessionIdentity(bootstrapSession.id, rawSenderId!, externalChatId);
714
+ bindSessionIdentity(bootstrapSession.id, rawSenderId!, conversationExternalId);
704
715
 
705
716
  // Transition bootstrap session to awaiting_response
706
717
  updateSessionStatus(bootstrapSession.id, 'awaiting_response');
@@ -711,9 +722,9 @@ export async function handleChannelInbound(
711
722
  assistantId: canonicalAssistantId,
712
723
  channel: sourceChannel,
713
724
  expectedExternalUserId: rawSenderId!,
714
- expectedChatId: externalChatId,
725
+ expectedChatId: conversationExternalId,
715
726
  identityBindingStatus: 'bound',
716
- destinationAddress: externalChatId,
727
+ destinationAddress: conversationExternalId,
717
728
  });
718
729
 
719
730
  // Compose and send the verification prompt via Telegram
@@ -726,7 +737,7 @@ export async function handleChannelInbound(
726
737
  );
727
738
 
728
739
  // Deliver verification Telegram message via the gateway (fire-and-forget)
729
- deliverBootstrapVerificationTelegram(externalChatId, telegramBody, canonicalAssistantId);
740
+ deliverBootstrapVerificationTelegram(conversationExternalId, telegramBody, canonicalAssistantId);
730
741
 
731
742
  // Update delivery tracking
732
743
  const now = Date.now();
@@ -769,9 +780,9 @@ export async function handleChannelInbound(
769
780
  sourceChannel,
770
781
  guardianVerifyCode,
771
782
  canonicalSenderId ?? rawSenderId!,
772
- externalChatId,
773
- body.senderUsername,
774
- body.senderName,
783
+ conversationExternalId,
784
+ body.actorUsername,
785
+ body.actorDisplayName,
775
786
  );
776
787
 
777
788
  const guardianVerifyOutcome: 'verified' | 'failed' = verifyResult.success ? 'verified' : 'failed';
@@ -782,7 +793,7 @@ export async function handleChannelInbound(
782
793
  assistantId: canonicalAssistantId,
783
794
  sourceChannel,
784
795
  externalUserId: canonicalSenderId ?? rawSenderId!,
785
- externalChatId,
796
+ externalChatId: conversationExternalId,
786
797
  })
787
798
  : null;
788
799
  const memberMatchesSender = existingMember?.externalUserId
@@ -790,18 +801,18 @@ export async function handleChannelInbound(
790
801
  : false;
791
802
  const preservedDisplayName = memberMatchesSender && existingMember?.displayName?.trim().length
792
803
  ? existingMember.displayName
793
- : body.senderName;
804
+ : body.actorDisplayName;
794
805
 
795
806
  upsertMember({
796
807
  assistantId: canonicalAssistantId,
797
808
  sourceChannel,
798
809
  externalUserId: canonicalSenderId ?? rawSenderId!,
799
- externalChatId,
810
+ externalChatId: conversationExternalId,
800
811
  status: 'active',
801
812
  policy: 'allow',
802
813
  // Keep guardian-curated member name stable across re-verification.
803
814
  displayName: preservedDisplayName,
804
- username: body.senderUsername,
815
+ username: body.actorUsername,
805
816
  });
806
817
 
807
818
  const verifyLogLabel = verifyResult.verificationType === 'trusted_contact'
@@ -826,10 +837,10 @@ export async function handleChannelInbound(
826
837
  },
827
838
  contextPayload: {
828
839
  sourceChannel,
829
- externalUserId: canonicalSenderId ?? rawSenderId!,
830
- externalChatId,
831
- senderName: body.senderName ?? null,
832
- senderUsername: body.senderUsername ?? null,
840
+ actorExternalId: canonicalSenderId ?? rawSenderId!,
841
+ conversationExternalId,
842
+ actorDisplayName: body.actorDisplayName ?? null,
843
+ actorUsername: body.actorUsername ?? null,
833
844
  },
834
845
  dedupeKey: `trusted-contact:activated:${canonicalAssistantId}:${sourceChannel}:${canonicalSenderId ?? rawSenderId!}`,
835
846
  });
@@ -851,7 +862,7 @@ export async function handleChannelInbound(
851
862
  }
852
863
  try {
853
864
  await deliverChannelReply(replyCallbackUrl, {
854
- chatId: externalChatId,
865
+ chatId: conversationExternalId,
855
866
  text: replyText,
856
867
  assistantId,
857
868
  }, bearerToken);
@@ -860,9 +871,9 @@ export async function handleChannelInbound(
860
871
  // we cannot simply re-throw and let the gateway retry the full
861
872
  // flow. Instead, persist the reply so that gateway retries
862
873
  // (which arrive as duplicates) can re-attempt delivery.
863
- log.error({ err, externalChatId }, 'Failed to deliver deterministic verification reply; persisting for retry');
874
+ log.error({ err, conversationExternalId }, 'Failed to deliver deterministic verification reply; persisting for retry');
864
875
  channelDeliveryStore.storePendingVerificationReply(result.eventId, {
865
- chatId: externalChatId,
876
+ chatId: conversationExternalId,
866
877
  text: replyText,
867
878
  assistantId,
868
879
  });
@@ -874,7 +885,7 @@ export async function handleChannelInbound(
874
885
  setTimeout(async () => {
875
886
  try {
876
887
  await deliverChannelReply(replyCallbackUrl, {
877
- chatId: externalChatId,
888
+ chatId: conversationExternalId,
878
889
  text: replyText,
879
890
  assistantId,
880
891
  }, bearerToken);
@@ -914,10 +925,10 @@ export async function handleChannelInbound(
914
925
  const guardianCtx: GuardianContext = resolveGuardianContext({
915
926
  assistantId: canonicalAssistantId,
916
927
  sourceChannel,
917
- externalChatId,
918
- senderExternalUserId: rawSenderId,
919
- senderUsername: body.senderUsername,
920
- senderDisplayName: body.senderName,
928
+ conversationExternalId,
929
+ actorExternalId: rawSenderId,
930
+ actorUsername: body.actorUsername,
931
+ actorDisplayName: body.actorDisplayName,
921
932
  });
922
933
 
923
934
  // Hoisted flag: set by the canonical guardian reply router when the invite
@@ -949,7 +960,7 @@ export async function handleChannelInbound(
949
960
  // router's own identity-based fallback stays active.
950
961
  const deliveryScopedPendingRequests = listPendingCanonicalGuardianRequestsByDestinationChat(
951
962
  sourceChannel,
952
- externalChatId,
963
+ conversationExternalId,
953
964
  );
954
965
  let pendingRequestIds: string[] | undefined;
955
966
  if (deliveryScopedPendingRequests.length > 0) {
@@ -972,7 +983,7 @@ export async function handleChannelInbound(
972
983
  actor: {
973
984
  externalUserId: canonicalSenderId ?? rawSenderId!,
974
985
  channel: sourceChannel,
975
- isTrusted: false,
986
+ guardianPrincipalId: guardianCtx.guardianPrincipalId ?? undefined,
976
987
  },
977
988
  conversationId: result.conversationId,
978
989
  callbackData: body.callbackData,
@@ -980,7 +991,7 @@ export async function handleChannelInbound(
980
991
  approvalConversationGenerator,
981
992
  channelDeliveryContext: {
982
993
  replyCallbackUrl,
983
- guardianChatId: externalChatId,
994
+ guardianChatId: conversationExternalId,
984
995
  assistantId: canonicalAssistantId,
985
996
  bearerToken,
986
997
  },
@@ -991,12 +1002,12 @@ export async function handleChannelInbound(
991
1002
  if (routerResult.replyText) {
992
1003
  try {
993
1004
  await deliverChannelReply(replyCallbackUrl, {
994
- chatId: externalChatId,
1005
+ chatId: conversationExternalId,
995
1006
  text: routerResult.replyText,
996
1007
  assistantId: canonicalAssistantId,
997
1008
  }, bearerToken);
998
1009
  } catch (err) {
999
- log.error({ err, externalChatId }, 'Failed to deliver canonical router reply');
1010
+ log.error({ err, conversationExternalId }, 'Failed to deliver canonical router reply');
1000
1011
  }
1001
1012
  }
1002
1013
 
@@ -1028,9 +1039,9 @@ export async function handleChannelInbound(
1028
1039
  conversationId: result.conversationId,
1029
1040
  callbackData: body.callbackData,
1030
1041
  content: trimmedContent,
1031
- externalChatId,
1042
+ conversationExternalId,
1032
1043
  sourceChannel,
1033
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
1044
+ actorExternalId: canonicalSenderId ?? rawSenderId,
1034
1045
  replyCallbackUrl,
1035
1046
  bearerToken,
1036
1047
  guardianCtx,
@@ -1125,11 +1136,11 @@ export async function handleChannelInbound(
1125
1136
  // replayed. If the ingress check later detects secrets we clear it
1126
1137
  // before throwing, so secret-bearing content is never left on disk.
1127
1138
  channelDeliveryStore.storePayload(result.eventId, {
1128
- sourceChannel, externalChatId, externalMessageId, content,
1139
+ sourceChannel, externalChatId: conversationExternalId, externalMessageId, content,
1129
1140
  attachmentIds, sourceMetadata: body.sourceMetadata,
1130
- senderName: body.senderName,
1131
- senderExternalUserId: body.senderExternalUserId,
1132
- senderUsername: body.senderUsername,
1141
+ senderName: body.actorDisplayName,
1142
+ senderExternalUserId: body.actorExternalId,
1143
+ senderUsername: body.actorUsername,
1133
1144
  guardianCtx: toGuardianRuntimeContext(sourceChannel, guardianCtx),
1134
1145
  replyCallbackUrl,
1135
1146
  assistantId: canonicalAssistantId,
@@ -1183,7 +1194,7 @@ export async function handleChannelInbound(
1183
1194
  attachmentIds: hasAttachments ? attachmentIds : undefined,
1184
1195
  sourceChannel,
1185
1196
  sourceInterface,
1186
- externalChatId,
1197
+ externalChatId: conversationExternalId,
1187
1198
  guardianCtx,
1188
1199
  metadataHints,
1189
1200
  metadataUxBrief,
@@ -1272,7 +1283,7 @@ async function handleInviteTokenIntercept(params: {
1272
1283
  });
1273
1284
 
1274
1285
  log.info(
1275
- { sourceChannel, externalChatId, ok: outcome.ok, type: outcome.ok ? outcome.type : undefined, reason: !outcome.ok ? outcome.reason : undefined },
1286
+ { sourceChannel, externalChatId: params.externalChatId, ok: outcome.ok, type: outcome.ok ? outcome.type : undefined, reason: !outcome.ok ? outcome.reason : undefined },
1276
1287
  'Invite token intercept: redemption result',
1277
1288
  );
1278
1289
 
@@ -123,6 +123,7 @@ export function createOrReuseToolGrantRequest(
123
123
  requesterExternalUserId,
124
124
  requesterChatId: requesterChatId ?? undefined,
125
125
  guardianExternalUserId: binding.guardianExternalUserId,
126
+ guardianPrincipalId: binding.guardianPrincipalId ?? undefined,
126
127
  toolName,
127
128
  inputDigest,
128
129
  questionText,
@@ -99,6 +99,16 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
99
99
  );
100
100
  }
101
101
 
102
+ // When stdout is not a TTY (e.g. desktop app redirects to a hatch log file),
103
+ // write to the rotating file only — the hatch log already captured early
104
+ // startup output and echoing pino output there is unnecessary duplication.
105
+ if (!process.stdout.isTTY) {
106
+ return pino(
107
+ { name: 'assistant', level, serializers: logSerializers },
108
+ fileStream,
109
+ );
110
+ }
111
+
102
112
  return pino(
103
113
  { name: 'assistant', level, serializers: logSerializers },
104
114
  pino.multistream([
@@ -1,59 +0,0 @@
1
- import {
2
- buildPointerGenerationPrompt,
3
- getPointerFallbackMessage,
4
- includesRequiredFacts,
5
- POINTER_COPY_MAX_TOKENS,
6
- POINTER_COPY_SYSTEM_PROMPT,
7
- POINTER_COPY_TIMEOUT_MS,
8
- } from '../calls/call-pointer-message-composer.js';
9
- import { loadConfig } from '../config/loader.js';
10
- import { getFailoverProvider } from '../providers/registry.js';
11
- import type { PointerCopyGenerator } from '../runtime/http-types.js';
12
-
13
- /**
14
- * Create the daemon-owned pointer copy generator that resolves providers
15
- * and calls `provider.sendMessage` to generate pointer message text.
16
- * This keeps all provider awareness in the daemon lifecycle, away from
17
- * the runtime composer.
18
- */
19
- export function createPointerCopyGenerator(): PointerCopyGenerator {
20
- return async (context, options = {}) => {
21
- const config = loadConfig();
22
- let provider;
23
- try {
24
- provider = getFailoverProvider(config.provider, config.providerOrder);
25
- } catch {
26
- return null;
27
- }
28
-
29
- const fallbackText = options.fallbackText?.trim() || getPointerFallbackMessage(context);
30
- const requiredFacts = options.requiredFacts
31
- ?.map((f) => f.trim())
32
- .filter((f) => f.length > 0);
33
- const prompt = buildPointerGenerationPrompt(context, fallbackText, requiredFacts);
34
-
35
- const response = await provider.sendMessage(
36
- [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
37
- [],
38
- POINTER_COPY_SYSTEM_PROMPT,
39
- {
40
- config: {
41
- max_tokens: options.maxTokens ?? POINTER_COPY_MAX_TOKENS,
42
- modelIntent: 'latency-optimized',
43
- },
44
- signal: AbortSignal.timeout(options.timeoutMs ?? POINTER_COPY_TIMEOUT_MS),
45
- },
46
- );
47
-
48
- const block = response.content.find((entry) => entry.type === 'text');
49
- const text = block && 'text' in block ? block.text.trim() : '';
50
- if (!text) return null;
51
- const cleaned = text
52
- .replace(/^["'`]+/, '')
53
- .replace(/["'`]+$/, '')
54
- .trim();
55
- if (!cleaned) return null;
56
- if (!includesRequiredFacts(cleaned, requiredFacts)) return null;
57
- return cleaned;
58
- };
59
- }