@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.
- package/ARCHITECTURE.md +4 -4
- package/README.md +6 -6
- package/bun.lock +6 -2
- package/docs/architecture/memory.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/actor-token-service.test.ts +5 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +78 -0
- package/src/__tests__/call-domain.test.ts +148 -10
- package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
- package/src/__tests__/call-pointer-messages.test.ts +105 -43
- package/src/__tests__/canonical-guardian-store.test.ts +44 -10
- package/src/__tests__/channel-approval-routes.test.ts +67 -65
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
- package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- package/src/__tests__/guardian-grant-minting.test.ts +24 -24
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
- package/src/__tests__/guardian-routing-state.test.ts +4 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
- package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +50 -47
- package/src/__tests__/relay-server.test.ts +71 -0
- package/src/__tests__/send-endpoint-busy.test.ts +6 -0
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
- package/src/__tests__/system-prompt.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +9 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/approvals/guardian-decision-primitive.ts +29 -25
- package/src/approvals/guardian-request-resolvers.ts +9 -5
- package/src/calls/call-pointer-message-composer.ts +27 -85
- package/src/calls/call-pointer-messages.ts +54 -21
- package/src/calls/guardian-dispatch.ts +30 -0
- package/src/calls/relay-server.ts +13 -13
- package/src/config/system-prompt.ts +10 -3
- package/src/config/templates/BOOTSTRAP.md +6 -5
- package/src/config/templates/USER.md +1 -0
- package/src/config/user-reference.ts +44 -0
- package/src/daemon/handlers/guardian-actions.ts +5 -2
- package/src/daemon/handlers/sessions.ts +8 -3
- package/src/daemon/lifecycle.ts +109 -3
- package/src/daemon/server.ts +32 -24
- package/src/daemon/session-agent-loop.ts +4 -3
- package/src/daemon/session-lifecycle.ts +1 -9
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -0
- package/src/daemon/session-tool-setup.ts +10 -0
- package/src/daemon/session.ts +1 -0
- package/src/memory/canonical-guardian-store.ts +40 -0
- package/src/memory/conversation-crud.ts +26 -0
- package/src/memory/conversation-store.ts +1 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +3 -0
- package/src/notifications/copy-composer.ts +2 -2
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -0
- package/src/runtime/guardian-context-resolver.ts +6 -2
- package/src/runtime/guardian-reply-router.ts +33 -16
- package/src/runtime/guardian-vellum-migration.ts +29 -5
- package/src/runtime/http-types.ts +0 -13
- package/src/runtime/local-actor-identity.ts +19 -13
- package/src/runtime/middleware/actor-token.ts +2 -2
- package/src/runtime/routes/channel-delivery-routes.ts +5 -5
- package/src/runtime/routes/conversation-routes.ts +45 -35
- package/src/runtime/routes/guardian-action-routes.ts +7 -1
- package/src/runtime/routes/guardian-approval-interception.ts +52 -52
- package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/util/logger.ts +10 -0
- 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
|
-
|
|
122
|
+
conversationExternalId?: string;
|
|
123
123
|
externalMessageId?: string;
|
|
124
124
|
content?: string;
|
|
125
125
|
isEdit?: boolean;
|
|
126
|
-
|
|
126
|
+
actorDisplayName?: string;
|
|
127
127
|
attachmentIds?: string[];
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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 (!
|
|
165
|
-
return httpError('BAD_REQUEST', '
|
|
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
|
|
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.
|
|
197
|
-
? String(body.
|
|
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
|
|
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.
|
|
307
|
-
senderUsername: body.
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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,
|
|
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:
|
|
346
|
+
chatId: conversationExternalId,
|
|
344
347
|
text: replyText,
|
|
345
348
|
assistantId,
|
|
346
349
|
}, bearerToken);
|
|
347
350
|
} catch (err) {
|
|
348
|
-
log.error({ err,
|
|
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.
|
|
393
|
-
senderUsername: body.
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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,
|
|
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:
|
|
435
|
+
chatId: conversationExternalId,
|
|
433
436
|
text: replyText,
|
|
434
437
|
assistantId,
|
|
435
438
|
}, bearerToken);
|
|
436
439
|
} catch (err) {
|
|
437
|
-
log.error({ err,
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
592
|
-
username: body.
|
|
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.
|
|
614
|
-
senderExternalUserId: body.
|
|
615
|
-
senderUsername: body.
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
650
|
-
senderIdentifier: body.
|
|
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!,
|
|
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:
|
|
725
|
+
expectedChatId: conversationExternalId,
|
|
715
726
|
identityBindingStatus: 'bound',
|
|
716
|
-
destinationAddress:
|
|
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(
|
|
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
|
-
|
|
773
|
-
body.
|
|
774
|
-
body.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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:
|
|
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,
|
|
874
|
+
log.error({ err, conversationExternalId }, 'Failed to deliver deterministic verification reply; persisting for retry');
|
|
864
875
|
channelDeliveryStore.storePendingVerificationReply(result.eventId, {
|
|
865
|
-
chatId:
|
|
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:
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1005
|
+
chatId: conversationExternalId,
|
|
995
1006
|
text: routerResult.replyText,
|
|
996
1007
|
assistantId: canonicalAssistantId,
|
|
997
1008
|
}, bearerToken);
|
|
998
1009
|
} catch (err) {
|
|
999
|
-
log.error({ err,
|
|
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
|
-
|
|
1042
|
+
conversationExternalId,
|
|
1032
1043
|
sourceChannel,
|
|
1033
|
-
|
|
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.
|
|
1131
|
-
senderExternalUserId: body.
|
|
1132
|
-
senderUsername: body.
|
|
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,
|
package/src/util/logger.ts
CHANGED
|
@@ -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
|
-
}
|