@vellumai/assistant 0.3.18 → 0.3.20
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 +155 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +605 -104
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/checker.test.ts +60 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +779 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +185 -1
- package/src/__tests__/guardian-grant-minting.test.ts +532 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +58 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trust-store.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +276 -212
- package/src/calls/call-domain.ts +56 -6
- package/src/calls/guardian-dispatch.ts +56 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +59 -4
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +26 -0
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +260 -422
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +13 -1
- package/src/memory/embedding-local.ts +22 -8
- package/src/memory/guardian-action-store.ts +133 -2
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +35 -1
- package/src/memory/scoped-approval-grants.ts +518 -0
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/permissions/checker.ts +27 -0
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +154 -0
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -74
- package/src/runtime/routes/inbound-message-handler.ts +568 -409
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { createAssistantMessage,createUserMessage } from '../agent/message-types.js';
|
|
10
10
|
import { answerCall } from '../calls/call-domain.js';
|
|
11
|
+
import { isTerminalState } from '../calls/call-state-machine.js';
|
|
12
|
+
import { getCallSession } from '../calls/call-store.js';
|
|
11
13
|
import type { TurnChannelContext, TurnInterfaceContext } from '../channels/types.js';
|
|
12
14
|
import { parseChannelId, parseInterfaceId } from '../channels/types.js';
|
|
13
15
|
import { getConfig } from '../config/loader.js';
|
|
@@ -15,10 +17,12 @@ import * as conversationStore from '../memory/conversation-store.js';
|
|
|
15
17
|
import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
|
|
16
18
|
import {
|
|
17
19
|
finalizeFollowup,
|
|
20
|
+
getDeliveriesByRequestId,
|
|
18
21
|
getExpiredDeliveriesByConversation,
|
|
19
22
|
getFollowupDeliveriesByConversation,
|
|
20
23
|
getGuardianActionRequest,
|
|
21
24
|
getPendingDeliveriesByConversation,
|
|
25
|
+
getPendingRequestByCallSessionId,
|
|
22
26
|
progressFollowupState,
|
|
23
27
|
resolveGuardianActionRequest,
|
|
24
28
|
startFollowupFromExpiredRequest,
|
|
@@ -28,9 +32,11 @@ import { createPreference } from '../notifications/preferences-store.js';
|
|
|
28
32
|
import type { Message } from '../providers/types.js';
|
|
29
33
|
import { processGuardianFollowUpTurn } from '../runtime/guardian-action-conversation-turn.js';
|
|
30
34
|
import { executeFollowupAction } from '../runtime/guardian-action-followup-executor.js';
|
|
35
|
+
import { tryMintGuardianActionGrant } from '../runtime/guardian-action-grant-minter.js';
|
|
31
36
|
import { composeGuardianActionMessageGenerative } from '../runtime/guardian-action-message-composer.js';
|
|
32
|
-
import type { GuardianActionCopyGenerator, GuardianFollowUpConversationGenerator } from '../runtime/http-types.js';
|
|
37
|
+
import type { ApprovalConversationGenerator, GuardianActionCopyGenerator, GuardianFollowUpConversationGenerator } from '../runtime/http-types.js';
|
|
33
38
|
import { getLogger } from '../util/logger.js';
|
|
39
|
+
import { resolveGuardianInviteIntent } from './guardian-invite-intent.js';
|
|
34
40
|
import { resolveGuardianVerificationIntent } from './guardian-verification-intent.js';
|
|
35
41
|
import type { UsageStats } from './ipc-contract.js';
|
|
36
42
|
import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
|
|
@@ -50,6 +56,7 @@ const log = getLogger('session-process');
|
|
|
50
56
|
// generator through Session / DaemonServer constructors.
|
|
51
57
|
let _guardianFollowUpGenerator: GuardianFollowUpConversationGenerator | undefined;
|
|
52
58
|
let _guardianActionCopyGenerator: GuardianActionCopyGenerator | undefined;
|
|
59
|
+
let _approvalConversationGenerator: ApprovalConversationGenerator | undefined;
|
|
53
60
|
|
|
54
61
|
/** Inject the guardian follow-up conversation generator (called from lifecycle.ts). */
|
|
55
62
|
export function setGuardianFollowUpConversationGenerator(gen: GuardianFollowUpConversationGenerator): void {
|
|
@@ -61,6 +68,11 @@ export function setGuardianActionCopyGenerator(gen: GuardianActionCopyGenerator)
|
|
|
61
68
|
_guardianActionCopyGenerator = gen;
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
/** Inject the approval conversation generator (called from lifecycle.ts). */
|
|
72
|
+
export function setApprovalConversationGenerator(gen: ApprovalConversationGenerator): void {
|
|
73
|
+
_approvalConversationGenerator = gen;
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
/** Build a model_info event with fresh config data. */
|
|
65
77
|
function buildModelInfoEvent(): ServerMessage {
|
|
66
78
|
const config = getConfig();
|
|
@@ -104,6 +116,7 @@ export interface ProcessSessionContext {
|
|
|
104
116
|
/** Assistant identity — used for scoping notification preferences. */
|
|
105
117
|
readonly assistantId?: string;
|
|
106
118
|
guardianContext?: GuardianRuntimeContext;
|
|
119
|
+
ensureActorScopedHistory(): Promise<void>;
|
|
107
120
|
persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown>, displayContent?: string): Promise<string>;
|
|
108
121
|
runAgentLoop(
|
|
109
122
|
content: string,
|
|
@@ -293,6 +306,15 @@ export async function drainQueue(session: ProcessSessionContext, reason: QueueDr
|
|
|
293
306
|
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted in queue — forcing skill flow');
|
|
294
307
|
agentLoopContent = guardianIntent.rewrittenContent;
|
|
295
308
|
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
309
|
+
} else {
|
|
310
|
+
// Guardian invite intent interception — force invite management
|
|
311
|
+
// requests into the trusted-contacts skill flow.
|
|
312
|
+
const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
|
|
313
|
+
if (inviteIntent.kind === 'invite_management') {
|
|
314
|
+
log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted in queue — forcing skill flow');
|
|
315
|
+
agentLoopContent = inviteIntent.rewrittenContent;
|
|
316
|
+
session.preactivatedSkillIds = ['trusted-contacts'];
|
|
317
|
+
}
|
|
296
318
|
}
|
|
297
319
|
}
|
|
298
320
|
|
|
@@ -379,470 +401,277 @@ export async function processMessage(
|
|
|
379
401
|
options?: { isInteractive?: boolean },
|
|
380
402
|
displayContent?: string,
|
|
381
403
|
): Promise<string> {
|
|
404
|
+
await session.ensureActorScopedHistory();
|
|
382
405
|
session.currentActiveSurfaceId = activeSurfaceId;
|
|
383
406
|
session.currentPage = currentPage;
|
|
384
407
|
|
|
385
|
-
// ──
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Multiple deliveries across any state: require request code prefix for disambiguation
|
|
415
|
-
if (!matchedDelivery && pendingDeliveries.length >= 1) {
|
|
416
|
-
for (const d of pendingDeliveries) {
|
|
417
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
418
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
419
|
-
matchedDelivery = d;
|
|
420
|
-
answerText = content.slice(req.requestCode.length).trim();
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// If no pending delivery matched, check whether the code targets an
|
|
426
|
-
// expired or follow-up delivery. If so, skip the pending section entirely
|
|
427
|
-
// and let the message fall through to the expired/follow-up handlers below.
|
|
428
|
-
if (!matchedDelivery) {
|
|
429
|
-
let matchesOtherState = false;
|
|
430
|
-
for (const d of [...crossStateExpired, ...crossStateFollowup]) {
|
|
408
|
+
// ── Unified guardian action answer interception (mac channel) ──
|
|
409
|
+
// Deterministic priority matching: pending → follow-up → expired.
|
|
410
|
+
// When the guardian includes an explicit request code, match it across all
|
|
411
|
+
// states in priority order. When only one actionable request exists,
|
|
412
|
+
// auto-match without requiring a code prefix.
|
|
413
|
+
{
|
|
414
|
+
const allPending = getPendingDeliveriesByConversation(session.conversationId);
|
|
415
|
+
const allFollowup = getFollowupDeliveriesByConversation(session.conversationId);
|
|
416
|
+
const allExpired = getExpiredDeliveriesByConversation(session.conversationId);
|
|
417
|
+
const totalActionable = allPending.length + allFollowup.length + allExpired.length;
|
|
418
|
+
|
|
419
|
+
if (totalActionable > 0) {
|
|
420
|
+
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
421
|
+
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
422
|
+
|
|
423
|
+
// Try to parse an explicit request code from the message, in priority order
|
|
424
|
+
type CodeMatch = { delivery: typeof allPending[0]; request: NonNullable<ReturnType<typeof getGuardianActionRequest>>; state: 'pending' | 'followup' | 'expired'; answerText: string };
|
|
425
|
+
let codeMatch: CodeMatch | null = null;
|
|
426
|
+
const upperContent = content.toUpperCase();
|
|
427
|
+
const orderedSets: Array<{ deliveries: typeof allPending; state: 'pending' | 'followup' | 'expired' }> = [
|
|
428
|
+
{ deliveries: allPending, state: 'pending' },
|
|
429
|
+
{ deliveries: allFollowup, state: 'followup' },
|
|
430
|
+
{ deliveries: allExpired, state: 'expired' },
|
|
431
|
+
];
|
|
432
|
+
for (const { deliveries, state } of orderedSets) {
|
|
433
|
+
for (const d of deliveries) {
|
|
431
434
|
const req = getGuardianActionRequest(d.requestId);
|
|
432
|
-
if (req &&
|
|
433
|
-
|
|
435
|
+
if (req && upperContent.startsWith(req.requestCode)) {
|
|
436
|
+
codeMatch = { delivery: d, request: req, state, answerText: content.slice(req.requestCode.length).trim() };
|
|
434
437
|
break;
|
|
435
438
|
}
|
|
436
439
|
}
|
|
437
|
-
|
|
438
|
-
if (!matchesOtherState) {
|
|
439
|
-
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
440
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
441
|
-
const userMsg = createUserMessage(content, attachments);
|
|
442
|
-
const persisted = await conversationStore.addMessage(
|
|
443
|
-
session.conversationId,
|
|
444
|
-
'user',
|
|
445
|
-
JSON.stringify(userMsg.content),
|
|
446
|
-
guardianChannelMeta,
|
|
447
|
-
);
|
|
448
|
-
session.messages.push(userMsg);
|
|
449
|
-
|
|
450
|
-
// Include codes from all states so the guardian sees all options
|
|
451
|
-
const allDeliveries = [...pendingDeliveries, ...crossStateExpired, ...crossStateFollowup];
|
|
452
|
-
const codes = allDeliveries
|
|
453
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
454
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
455
|
-
const disambiguationText = `You have multiple pending guardian questions. Please prefix your reply with the reference code (${codes.join(', ')}) to indicate which question you are answering.`;
|
|
456
|
-
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
457
|
-
await conversationStore.addMessage(
|
|
458
|
-
session.conversationId,
|
|
459
|
-
'assistant',
|
|
460
|
-
JSON.stringify(disambiguationMsg.content),
|
|
461
|
-
guardianChannelMeta,
|
|
462
|
-
);
|
|
463
|
-
session.messages.push(disambiguationMsg);
|
|
464
|
-
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
465
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
466
|
-
return persisted.id;
|
|
467
|
-
}
|
|
468
|
-
// Code matched an expired/follow-up delivery — fall through to those handlers
|
|
440
|
+
if (codeMatch) break;
|
|
469
441
|
}
|
|
470
|
-
}
|
|
471
442
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const answerResult = await answerCall({ callSessionId: guardianRequest.callSessionId, answer: answerText });
|
|
491
|
-
|
|
492
|
-
if ('ok' in answerResult && answerResult.ok) {
|
|
493
|
-
const resolved = resolveGuardianActionRequest(guardianRequest.id, answerText, 'vellum');
|
|
494
|
-
const replyText = resolved
|
|
495
|
-
? 'Your answer has been relayed to the call.'
|
|
496
|
-
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_answered' }, {}, _guardianActionCopyGenerator);
|
|
497
|
-
const replyMsg = createAssistantMessage(replyText);
|
|
498
|
-
await conversationStore.addMessage(
|
|
499
|
-
session.conversationId,
|
|
500
|
-
'assistant',
|
|
501
|
-
JSON.stringify(replyMsg.content),
|
|
502
|
-
guardianChannelMeta,
|
|
503
|
-
);
|
|
504
|
-
session.messages.push(replyMsg);
|
|
505
|
-
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
506
|
-
} else {
|
|
507
|
-
const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
508
|
-
log.warn({ callSessionId: guardianRequest.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer');
|
|
509
|
-
const failureText = await composeGuardianActionMessageGenerative(
|
|
510
|
-
{ scenario: 'guardian_answer_delivery_failed' },
|
|
511
|
-
{},
|
|
512
|
-
_guardianActionCopyGenerator,
|
|
513
|
-
);
|
|
514
|
-
const failMsg = createAssistantMessage(failureText);
|
|
515
|
-
await conversationStore.addMessage(
|
|
516
|
-
session.conversationId,
|
|
517
|
-
'assistant',
|
|
518
|
-
JSON.stringify(failMsg.content),
|
|
519
|
-
guardianChannelMeta,
|
|
520
|
-
);
|
|
521
|
-
session.messages.push(failMsg);
|
|
522
|
-
onEvent({ type: 'assistant_text_delta', text: failureText });
|
|
443
|
+
// Explicit code targets a non-pending state: handle terminal superseded
|
|
444
|
+
if (codeMatch && codeMatch.state !== 'pending') {
|
|
445
|
+
const targetReq = codeMatch.request;
|
|
446
|
+
if (targetReq.status === 'expired' && targetReq.expiredReason === 'superseded') {
|
|
447
|
+
const callSession = getCallSession(targetReq.callSessionId);
|
|
448
|
+
const callStillActive = callSession && !isTerminalState(callSession.status);
|
|
449
|
+
if (!callStillActive) {
|
|
450
|
+
const userMsg = createUserMessage(content, attachments);
|
|
451
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
452
|
+
session.messages.push(userMsg);
|
|
453
|
+
const staleText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_superseded' }, {}, _guardianActionCopyGenerator);
|
|
454
|
+
const staleMsg = createAssistantMessage(staleText);
|
|
455
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(staleMsg.content), guardianChannelMeta);
|
|
456
|
+
session.messages.push(staleMsg);
|
|
457
|
+
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
458
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
459
|
+
return persisted.id;
|
|
460
|
+
}
|
|
523
461
|
}
|
|
524
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
525
|
-
return persisted.id;
|
|
526
462
|
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
463
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const expCrossStateFollowup = getFollowupDeliveriesByConversation(session.conversationId);
|
|
542
|
-
const expTotalCrossStateCount = expiredDeliveries.length + expCrossStatePending.length + expCrossStateFollowup.length;
|
|
543
|
-
let matchedExpired = (expiredDeliveries.length === 1 && expTotalCrossStateCount === 1) ? expiredDeliveries[0] : null;
|
|
544
|
-
let expiredAnswerText = content;
|
|
545
|
-
|
|
546
|
-
// Strip the request code prefix from the answer text when the single
|
|
547
|
-
// expired delivery is auto-matched (content may include a code prefix
|
|
548
|
-
// if the pending section fell through via matchesOtherState).
|
|
549
|
-
if (matchedExpired) {
|
|
550
|
-
const req = getGuardianActionRequest(matchedExpired.requestId);
|
|
551
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
552
|
-
expiredAnswerText = content.slice(req.requestCode.length).trim();
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Multiple expired deliveries (or cross-state disambiguation needed):
|
|
557
|
-
// require request code prefix for disambiguation
|
|
558
|
-
if (!matchedExpired && expiredDeliveries.length >= 1) {
|
|
559
|
-
for (const d of expiredDeliveries) {
|
|
560
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
561
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
562
|
-
matchedExpired = d;
|
|
563
|
-
expiredAnswerText = content.slice(req.requestCode.length).trim();
|
|
564
|
-
break;
|
|
464
|
+
// Auto-match: single actionable request across all states
|
|
465
|
+
if (!codeMatch && totalActionable === 1) {
|
|
466
|
+
const singleDelivery = allPending[0] ?? allFollowup[0] ?? allExpired[0];
|
|
467
|
+
const singleReq = getGuardianActionRequest(singleDelivery.requestId);
|
|
468
|
+
if (singleReq) {
|
|
469
|
+
const state: 'pending' | 'followup' | 'expired' = allPending.length === 1 ? 'pending' : allFollowup.length === 1 ? 'followup' : 'expired';
|
|
470
|
+
let text = content;
|
|
471
|
+
if (upperContent.startsWith(singleReq.requestCode)) {
|
|
472
|
+
text = content.slice(singleReq.requestCode.length).trim();
|
|
473
|
+
}
|
|
474
|
+
codeMatch = { delivery: singleDelivery, request: singleReq, state, answerText: text };
|
|
565
475
|
}
|
|
566
476
|
}
|
|
567
477
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
478
|
+
// Unknown code: message starts with a 6-char alphanumeric token that doesn't match
|
|
479
|
+
if (!codeMatch && totalActionable > 0) {
|
|
480
|
+
const possibleCodeMatch = content.match(/^([A-F0-9]{6})\s/i);
|
|
481
|
+
if (possibleCodeMatch) {
|
|
482
|
+
const candidateCode = possibleCodeMatch[1].toUpperCase();
|
|
483
|
+
const allDeliveries = [...allPending, ...allFollowup, ...allExpired];
|
|
484
|
+
const knownCodes = allDeliveries
|
|
485
|
+
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req?.requestCode; })
|
|
486
|
+
.filter((code): code is string => typeof code === 'string');
|
|
487
|
+
if (!knownCodes.includes(candidateCode)) {
|
|
488
|
+
const userMsg = createUserMessage(content, attachments);
|
|
489
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
490
|
+
session.messages.push(userMsg);
|
|
491
|
+
const unknownText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_unknown_code', unknownCode: candidateCode }, {}, _guardianActionCopyGenerator);
|
|
492
|
+
const unknownMsg = createAssistantMessage(unknownText);
|
|
493
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(unknownMsg.content), guardianChannelMeta);
|
|
494
|
+
session.messages.push(unknownMsg);
|
|
495
|
+
onEvent({ type: 'assistant_text_delta', text: unknownText });
|
|
496
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
497
|
+
return persisted.id;
|
|
577
498
|
}
|
|
578
499
|
}
|
|
579
|
-
|
|
580
|
-
if (!matchesFollowupState) {
|
|
581
|
-
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
582
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
583
|
-
const userMsg = createUserMessage(content, attachments);
|
|
584
|
-
const persisted = await conversationStore.addMessage(
|
|
585
|
-
session.conversationId,
|
|
586
|
-
'user',
|
|
587
|
-
JSON.stringify(userMsg.content),
|
|
588
|
-
guardianChannelMeta,
|
|
589
|
-
);
|
|
590
|
-
session.messages.push(userMsg);
|
|
591
|
-
|
|
592
|
-
// Include codes from all states so the guardian sees all options
|
|
593
|
-
const allExpiredDeliveries = [...expiredDeliveries, ...expCrossStatePending, ...expCrossStateFollowup];
|
|
594
|
-
const codes = allExpiredDeliveries
|
|
595
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
596
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
597
|
-
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
598
|
-
{ scenario: 'guardian_expired_disambiguation', requestCodes: codes },
|
|
599
|
-
{ requiredKeywords: codes },
|
|
600
|
-
_guardianActionCopyGenerator,
|
|
601
|
-
);
|
|
602
|
-
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
603
|
-
await conversationStore.addMessage(
|
|
604
|
-
session.conversationId,
|
|
605
|
-
'assistant',
|
|
606
|
-
JSON.stringify(disambiguationMsg.content),
|
|
607
|
-
guardianChannelMeta,
|
|
608
|
-
);
|
|
609
|
-
session.messages.push(disambiguationMsg);
|
|
610
|
-
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
611
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
612
|
-
return persisted.id;
|
|
613
|
-
}
|
|
614
|
-
// Code matched a follow-up or pending delivery — fall through to those handlers
|
|
615
500
|
}
|
|
616
|
-
}
|
|
617
501
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (expiredRequest && expiredRequest.status === 'expired' && expiredRequest.followupState === 'none') {
|
|
621
|
-
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
622
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
502
|
+
// No match and multiple actionable requests → disambiguation
|
|
503
|
+
if (!codeMatch && totalActionable > 1) {
|
|
623
504
|
const userMsg = createUserMessage(content, attachments);
|
|
624
|
-
const persisted = await conversationStore.addMessage(
|
|
625
|
-
session.conversationId,
|
|
626
|
-
'user',
|
|
627
|
-
JSON.stringify(userMsg.content),
|
|
628
|
-
guardianChannelMeta,
|
|
629
|
-
);
|
|
505
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
630
506
|
session.messages.push(userMsg);
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
);
|
|
650
|
-
session.messages.push(replyMsg);
|
|
651
|
-
onEvent({ type: 'assistant_text_delta', text: followupText });
|
|
652
|
-
} else {
|
|
653
|
-
// Follow-up already started or conflict — send stale message
|
|
654
|
-
const staleText = await composeGuardianActionMessageGenerative(
|
|
655
|
-
{ scenario: 'guardian_stale_expired' },
|
|
656
|
-
{},
|
|
657
|
-
_guardianActionCopyGenerator,
|
|
658
|
-
);
|
|
659
|
-
const staleMsg = createAssistantMessage(staleText);
|
|
660
|
-
await conversationStore.addMessage(
|
|
661
|
-
session.conversationId,
|
|
662
|
-
'assistant',
|
|
663
|
-
JSON.stringify(staleMsg.content),
|
|
664
|
-
guardianChannelMeta,
|
|
665
|
-
);
|
|
666
|
-
session.messages.push(staleMsg);
|
|
667
|
-
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
668
|
-
}
|
|
507
|
+
const allDeliveries = [...allPending, ...allFollowup, ...allExpired];
|
|
508
|
+
const codes = allDeliveries
|
|
509
|
+
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
510
|
+
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
511
|
+
const disambiguationScenario = allPending.length > 0
|
|
512
|
+
? 'guardian_pending_disambiguation' as const
|
|
513
|
+
: allFollowup.length > 0
|
|
514
|
+
? 'guardian_followup_disambiguation' as const
|
|
515
|
+
: 'guardian_expired_disambiguation' as const;
|
|
516
|
+
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
517
|
+
{ scenario: disambiguationScenario, requestCodes: codes },
|
|
518
|
+
{ requiredKeywords: codes },
|
|
519
|
+
_guardianActionCopyGenerator,
|
|
520
|
+
);
|
|
521
|
+
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
522
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(disambiguationMsg.content), guardianChannelMeta);
|
|
523
|
+
session.messages.push(disambiguationMsg);
|
|
524
|
+
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
669
525
|
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
670
526
|
return persisted.id;
|
|
671
527
|
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
528
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// is the reply — route it through the conversation engine. When multiple
|
|
679
|
-
// follow-up deliveries exist, require request-code prefix for disambiguation.
|
|
680
|
-
const followupDeliveries = getFollowupDeliveriesByConversation(session.conversationId);
|
|
681
|
-
if (followupDeliveries.length > 0) {
|
|
682
|
-
// Cross-state disambiguation: check total deliveries across all states
|
|
683
|
-
// (pending + expired + follow-up). When the total exceeds 1, require
|
|
684
|
-
// request-code disambiguation even for a lone follow-up delivery.
|
|
685
|
-
const fuCrossStatePending = getPendingDeliveriesByConversation(session.conversationId);
|
|
686
|
-
const fuCrossStateExpired = getExpiredDeliveriesByConversation(session.conversationId);
|
|
687
|
-
const fuTotalCrossStateCount = followupDeliveries.length + fuCrossStatePending.length + fuCrossStateExpired.length;
|
|
688
|
-
let matchedFollowup = (followupDeliveries.length === 1 && fuTotalCrossStateCount === 1) ? followupDeliveries[0] : null;
|
|
689
|
-
let followupReplyText = content;
|
|
690
|
-
|
|
691
|
-
// Strip the request code prefix from the reply text when the single
|
|
692
|
-
// follow-up delivery is auto-matched (content may include a code prefix
|
|
693
|
-
// if the pending section fell through via matchesOtherState).
|
|
694
|
-
if (matchedFollowup) {
|
|
695
|
-
const req = getGuardianActionRequest(matchedFollowup.requestId);
|
|
696
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
697
|
-
followupReplyText = content.slice(req.requestCode.length).trim();
|
|
698
|
-
}
|
|
699
|
-
}
|
|
529
|
+
// Dispatch matched delivery by state
|
|
530
|
+
if (codeMatch) {
|
|
531
|
+
const { request, state, answerText } = codeMatch;
|
|
700
532
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
707
|
-
matchedFollowup = d;
|
|
708
|
-
followupReplyText = content.slice(req.requestCode.length).trim();
|
|
709
|
-
break;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
533
|
+
// PENDING state handler
|
|
534
|
+
if (state === 'pending' && request.status === 'pending') {
|
|
535
|
+
const userMsg = createUserMessage(content, attachments);
|
|
536
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
537
|
+
session.messages.push(userMsg);
|
|
712
538
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
539
|
+
const answerResult = await answerCall({ callSessionId: request.callSessionId, answer: answerText, pendingQuestionId: request.pendingQuestionId });
|
|
540
|
+
|
|
541
|
+
if ('ok' in answerResult && answerResult.ok) {
|
|
542
|
+
const resolved = resolveGuardianActionRequest(request.id, answerText, 'vellum');
|
|
543
|
+
if (resolved) {
|
|
544
|
+
await tryMintGuardianActionGrant({ request, answerText, decisionChannel: 'vellum', approvalConversationGenerator: _approvalConversationGenerator });
|
|
545
|
+
}
|
|
546
|
+
const replyText = resolved
|
|
547
|
+
? 'Your answer has been relayed to the call.'
|
|
548
|
+
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_answered' }, {}, _guardianActionCopyGenerator);
|
|
549
|
+
const replyMsg = createAssistantMessage(replyText);
|
|
550
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
|
|
551
|
+
session.messages.push(replyMsg);
|
|
552
|
+
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
553
|
+
} else {
|
|
554
|
+
const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
555
|
+
log.warn({ callSessionId: request.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer');
|
|
556
|
+
const failureText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_answer_delivery_failed' }, {}, _guardianActionCopyGenerator);
|
|
557
|
+
const failMsg = createAssistantMessage(failureText);
|
|
558
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(failMsg.content), guardianChannelMeta);
|
|
559
|
+
session.messages.push(failMsg);
|
|
560
|
+
onEvent({ type: 'assistant_text_delta', text: failureText });
|
|
723
561
|
}
|
|
562
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
563
|
+
return persisted.id;
|
|
724
564
|
}
|
|
725
565
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
566
|
+
// FOLLOW-UP state handler
|
|
567
|
+
if (state === 'followup' && request.followupState === 'awaiting_guardian_choice') {
|
|
729
568
|
const userMsg = createUserMessage(content, attachments);
|
|
730
|
-
const persisted = await conversationStore.addMessage(
|
|
731
|
-
session.conversationId,
|
|
732
|
-
'user',
|
|
733
|
-
JSON.stringify(userMsg.content),
|
|
734
|
-
guardianChannelMeta,
|
|
735
|
-
);
|
|
569
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
736
570
|
session.messages.push(userMsg);
|
|
737
571
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
742
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
743
|
-
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
744
|
-
{ scenario: 'guardian_followup_disambiguation', requestCodes: codes },
|
|
745
|
-
{ requiredKeywords: codes },
|
|
746
|
-
_guardianActionCopyGenerator,
|
|
747
|
-
);
|
|
748
|
-
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
749
|
-
await conversationStore.addMessage(
|
|
750
|
-
session.conversationId,
|
|
751
|
-
'assistant',
|
|
752
|
-
JSON.stringify(disambiguationMsg.content),
|
|
753
|
-
guardianChannelMeta,
|
|
572
|
+
const turnResult = await processGuardianFollowUpTurn(
|
|
573
|
+
{ questionText: request.questionText, lateAnswerText: request.lateAnswerText ?? '', guardianReply: answerText },
|
|
574
|
+
_guardianFollowUpGenerator,
|
|
754
575
|
);
|
|
755
|
-
session.messages.push(disambiguationMsg);
|
|
756
|
-
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
757
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
758
|
-
return persisted.id;
|
|
759
|
-
}
|
|
760
|
-
// Code matched an expired or pending delivery — fall through to agent loop
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
576
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const persisted = await conversationStore.addMessage(
|
|
771
|
-
session.conversationId,
|
|
772
|
-
'user',
|
|
773
|
-
JSON.stringify(userMsg.content),
|
|
774
|
-
guardianChannelMeta,
|
|
775
|
-
);
|
|
776
|
-
session.messages.push(userMsg);
|
|
577
|
+
let stateApplied = true;
|
|
578
|
+
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
579
|
+
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !== undefined;
|
|
580
|
+
} else if (turnResult.disposition === 'decline') {
|
|
581
|
+
stateApplied = finalizeFollowup(request.id, 'declined') !== undefined;
|
|
582
|
+
}
|
|
777
583
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
lateAnswerText: followupRequest.lateAnswerText ?? '',
|
|
782
|
-
guardianReply: followupReplyText,
|
|
783
|
-
},
|
|
784
|
-
_guardianFollowUpGenerator,
|
|
785
|
-
);
|
|
584
|
+
if (!stateApplied) {
|
|
585
|
+
log.warn({ requestId: request.id, disposition: turnResult.disposition }, 'Follow-up state transition failed (already resolved)');
|
|
586
|
+
}
|
|
786
587
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
} else if (turnResult.disposition === 'decline') {
|
|
796
|
-
stateApplied = finalizeFollowup(followupRequest.id, 'declined') !== undefined;
|
|
797
|
-
}
|
|
798
|
-
// keep_pending: no state change — guardian can reply again
|
|
588
|
+
const replyText = stateApplied
|
|
589
|
+
? turnResult.replyText
|
|
590
|
+
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_followup' }, {}, _guardianActionCopyGenerator);
|
|
591
|
+
const replyMsg = createAssistantMessage(replyText);
|
|
592
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
|
|
593
|
+
session.messages.push(replyMsg);
|
|
594
|
+
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
595
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
799
596
|
|
|
800
|
-
|
|
801
|
-
|
|
597
|
+
if (stateApplied && (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back')) {
|
|
598
|
+
void (async () => {
|
|
599
|
+
try {
|
|
600
|
+
const execResult = await executeFollowupAction(request.id, turnResult.disposition as 'call_back' | 'message_back', _guardianActionCopyGenerator);
|
|
601
|
+
const completionMsg = createAssistantMessage(execResult.guardianReplyText);
|
|
602
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(completionMsg.content), guardianChannelMeta);
|
|
603
|
+
session.messages.push(completionMsg);
|
|
604
|
+
onEvent({ type: 'assistant_text_delta', text: execResult.guardianReplyText });
|
|
605
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
606
|
+
} catch (execErr) {
|
|
607
|
+
log.error({ err: execErr, requestId: request.id }, 'Follow-up action execution or completion message failed');
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
611
|
+
return persisted.id;
|
|
802
612
|
}
|
|
803
613
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
session.conversationId,
|
|
810
|
-
'assistant',
|
|
811
|
-
JSON.stringify(replyMsg.content),
|
|
812
|
-
guardianChannelMeta,
|
|
813
|
-
);
|
|
814
|
-
session.messages.push(replyMsg);
|
|
815
|
-
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
816
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
614
|
+
// EXPIRED state handler
|
|
615
|
+
if (state === 'expired' && request.status === 'expired' && request.followupState === 'none') {
|
|
616
|
+
const userMsg = createUserMessage(content, attachments);
|
|
617
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
618
|
+
session.messages.push(userMsg);
|
|
817
619
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
620
|
+
// Superseded remap
|
|
621
|
+
if (request.expiredReason === 'superseded') {
|
|
622
|
+
const callSession = getCallSession(request.callSessionId);
|
|
623
|
+
const callStillActive = callSession && !isTerminalState(callSession.status);
|
|
624
|
+
const currentPending = callStillActive ? getPendingRequestByCallSessionId(request.callSessionId) : null;
|
|
625
|
+
|
|
626
|
+
if (callStillActive && currentPending) {
|
|
627
|
+
const currentDeliveries = getDeliveriesByRequestId(currentPending.id);
|
|
628
|
+
const guardianExtUserId = session.guardianContext?.guardianExternalUserId;
|
|
629
|
+
// When guardianExternalUserId is present, verify the sender has a
|
|
630
|
+
// matching delivery on the current pending request. When it's absent
|
|
631
|
+
// (trusted Vellum/HTTP session), allow the remap without delivery check.
|
|
632
|
+
const senderHasDelivery = guardianExtUserId
|
|
633
|
+
? currentDeliveries.some((d) => d.destinationExternalUserId === guardianExtUserId)
|
|
634
|
+
: true;
|
|
635
|
+
if (!senderHasDelivery) {
|
|
636
|
+
log.info({ supersededRequestId: request.id, currentRequestId: currentPending.id, guardianExternalUserId: guardianExtUserId }, 'Superseded remap skipped: sender has no delivery on current pending request');
|
|
637
|
+
} else {
|
|
638
|
+
const remapResult = await answerCall({ callSessionId: currentPending.callSessionId, answer: answerText, pendingQuestionId: currentPending.pendingQuestionId });
|
|
639
|
+
if ('ok' in remapResult && remapResult.ok) {
|
|
640
|
+
const resolved = resolveGuardianActionRequest(currentPending.id, answerText, 'vellum');
|
|
641
|
+
if (resolved) {
|
|
642
|
+
await tryMintGuardianActionGrant({ request: currentPending, answerText, decisionChannel: 'vellum', approvalConversationGenerator: _approvalConversationGenerator });
|
|
643
|
+
}
|
|
644
|
+
const remapText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_superseded_remap', questionText: currentPending.questionText }, {}, _guardianActionCopyGenerator);
|
|
645
|
+
const remapMsg = createAssistantMessage(remapText);
|
|
646
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(remapMsg.content), guardianChannelMeta);
|
|
647
|
+
session.messages.push(remapMsg);
|
|
648
|
+
onEvent({ type: 'assistant_text_delta', text: remapText });
|
|
649
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
650
|
+
log.info({ supersededRequestId: request.id, remappedToRequestId: currentPending.id }, 'Late approval for superseded request remapped to current pending request');
|
|
651
|
+
return persisted.id;
|
|
652
|
+
}
|
|
653
|
+
log.warn({ callSessionId: currentPending.callSessionId, error: 'error' in remapResult ? remapResult.error : 'unknown' }, 'Superseded remap answerCall failed, falling through to follow-up');
|
|
654
|
+
}
|
|
841
655
|
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
656
|
+
}
|
|
844
657
|
|
|
845
|
-
|
|
658
|
+
const followupResult = startFollowupFromExpiredRequest(request.id, answerText);
|
|
659
|
+
if (followupResult) {
|
|
660
|
+
const followupText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_late_answer_followup', questionText: request.questionText, lateAnswerText: answerText }, {}, _guardianActionCopyGenerator);
|
|
661
|
+
const replyMsg = createAssistantMessage(followupText);
|
|
662
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
|
|
663
|
+
session.messages.push(replyMsg);
|
|
664
|
+
onEvent({ type: 'assistant_text_delta', text: followupText });
|
|
665
|
+
} else {
|
|
666
|
+
const staleText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_expired' }, {}, _guardianActionCopyGenerator);
|
|
667
|
+
const staleMsg = createAssistantMessage(staleText);
|
|
668
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(staleMsg.content), guardianChannelMeta);
|
|
669
|
+
session.messages.push(staleMsg);
|
|
670
|
+
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
671
|
+
}
|
|
672
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
673
|
+
return persisted.id;
|
|
674
|
+
}
|
|
846
675
|
}
|
|
847
676
|
}
|
|
848
677
|
}
|
|
@@ -930,6 +759,15 @@ export async function processMessage(
|
|
|
930
759
|
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted — forcing skill flow');
|
|
931
760
|
agentLoopContent = guardianIntent.rewrittenContent;
|
|
932
761
|
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
762
|
+
} else {
|
|
763
|
+
// Guardian invite intent interception — force invite management
|
|
764
|
+
// requests into the trusted-contacts skill flow.
|
|
765
|
+
const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
|
|
766
|
+
if (inviteIntent.kind === 'invite_management') {
|
|
767
|
+
log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted — forcing skill flow');
|
|
768
|
+
agentLoopContent = inviteIntent.rewrittenContent;
|
|
769
|
+
session.preactivatedSkillIds = ['trusted-contacts'];
|
|
770
|
+
}
|
|
933
771
|
}
|
|
934
772
|
}
|
|
935
773
|
|