@vellumai/assistant 0.4.30 → 0.4.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +1 -1
- package/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
- package/src/__tests__/config-schema.test.ts +6 -14
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/amazon/session.ts +30 -91
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +269 -899
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -10
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +19 -0
- package/src/config/memory-schema.ts +0 -10
- package/src/config/schema.ts +2 -2
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1337
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +0 -29
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +24 -7
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +125 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1402
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +83 -710
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +371 -29
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +192 -194
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +17 -2
- package/src/runtime/routes/guardian-action-routes.ts +23 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +31 -0
- package/src/runtime/routes/migration-routes.ts +47 -17
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/contacts/startup-migration.ts +0 -21
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
|
@@ -40,8 +40,8 @@ export interface ContactPayload {
|
|
|
40
40
|
id: string;
|
|
41
41
|
displayName: string;
|
|
42
42
|
role: "guardian" | "contact";
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
notes?: string;
|
|
44
|
+
contactType?: string;
|
|
45
45
|
lastInteraction?: number;
|
|
46
46
|
interactionCount: number;
|
|
47
47
|
channels: ContactChannelPayload[];
|
|
@@ -141,11 +141,6 @@ export interface UsageRequest {
|
|
|
141
141
|
sessionId: string;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export interface SandboxSetRequest {
|
|
145
|
-
type: "sandbox_set";
|
|
146
|
-
enabled: boolean;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
144
|
export interface SessionsClearRequest {
|
|
150
145
|
type: "sessions_clear";
|
|
151
146
|
}
|
|
@@ -421,7 +416,6 @@ export type _SessionsClientMessages =
|
|
|
421
416
|
| UndoRequest
|
|
422
417
|
| RegenerateRequest
|
|
423
418
|
| UsageRequest
|
|
424
|
-
| SandboxSetRequest
|
|
425
419
|
| SessionListRequest
|
|
426
420
|
| SessionCreateRequest
|
|
427
421
|
| SessionSwitchRequest
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import { loadConfig } from "../config/loader.js";
|
|
21
21
|
import { ensurePromptFiles } from "../config/system-prompt.js";
|
|
22
22
|
import { syncUpdateBulletinOnStartup } from "../config/update-bulletin.js";
|
|
23
|
-
import { migrateContactsFromLegacyTables } from "../contacts/startup-migration.js";
|
|
24
23
|
import { HeartbeatService } from "../heartbeat/heartbeat-service.js";
|
|
25
24
|
import { getHookManager } from "../hooks/manager.js";
|
|
26
25
|
import { installTemplates } from "../hooks/templates.js";
|
|
@@ -204,18 +203,6 @@ export async function runDaemon(): Promise<void> {
|
|
|
204
203
|
);
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
// Catch-up migration: populate contacts table from legacy guardian
|
|
208
|
-
// bindings and contact rows. Ensures upgrades from pre-contacts
|
|
209
|
-
// versions have a populated contacts table on first boot.
|
|
210
|
-
try {
|
|
211
|
-
migrateContactsFromLegacyTables("self");
|
|
212
|
-
} catch (err) {
|
|
213
|
-
log.warn(
|
|
214
|
-
{ err },
|
|
215
|
-
"Contacts startup migration failed — continuing startup",
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
206
|
try {
|
|
220
207
|
syncUpdateBulletinOnStartup();
|
|
221
208
|
} catch (err) {
|
|
@@ -437,22 +424,6 @@ export async function runDaemon(): Promise<void> {
|
|
|
437
424
|
sourceChannel,
|
|
438
425
|
sourceInterface,
|
|
439
426
|
),
|
|
440
|
-
persistAndProcessMessage: (
|
|
441
|
-
conversationId,
|
|
442
|
-
content,
|
|
443
|
-
attachmentIds,
|
|
444
|
-
options,
|
|
445
|
-
sourceChannel,
|
|
446
|
-
sourceInterface,
|
|
447
|
-
) =>
|
|
448
|
-
server.persistAndProcessMessage(
|
|
449
|
-
conversationId,
|
|
450
|
-
content,
|
|
451
|
-
attachmentIds,
|
|
452
|
-
options,
|
|
453
|
-
sourceChannel,
|
|
454
|
-
sourceInterface,
|
|
455
|
-
),
|
|
456
427
|
interfacesDir: getInterfacesDir(),
|
|
457
428
|
approvalCopyGenerator: createApprovalCopyGenerator(),
|
|
458
429
|
approvalConversationGenerator: createApprovalConversationGenerator(),
|
|
@@ -453,50 +453,7 @@ export async function runAgentLoopImpl(
|
|
|
453
453
|
onEvent,
|
|
454
454
|
);
|
|
455
455
|
|
|
456
|
-
|
|
457
|
-
const loopChannelMeta = {
|
|
458
|
-
...provenanceFromTrustContext(ctx.trustContext),
|
|
459
|
-
userMessageChannel: capturedTurnChannelContext.userMessageChannel,
|
|
460
|
-
assistantMessageChannel:
|
|
461
|
-
capturedTurnChannelContext.assistantMessageChannel,
|
|
462
|
-
userMessageInterface: capturedTurnInterfaceContext.userMessageInterface,
|
|
463
|
-
assistantMessageInterface:
|
|
464
|
-
capturedTurnInterfaceContext.assistantMessageInterface,
|
|
465
|
-
};
|
|
466
|
-
const assistantMessage = createAssistantMessage(
|
|
467
|
-
memoryResult.conflictClarification,
|
|
468
|
-
);
|
|
469
|
-
await conversationStore.addMessage(
|
|
470
|
-
ctx.conversationId,
|
|
471
|
-
"assistant",
|
|
472
|
-
JSON.stringify(assistantMessage.content),
|
|
473
|
-
loopChannelMeta,
|
|
474
|
-
);
|
|
475
|
-
ctx.messages.push(assistantMessage);
|
|
476
|
-
onEvent({
|
|
477
|
-
type: "assistant_text_delta",
|
|
478
|
-
text: memoryResult.conflictClarification,
|
|
479
|
-
sessionId: ctx.conversationId,
|
|
480
|
-
});
|
|
481
|
-
ctx.traceEmitter.emit(
|
|
482
|
-
"message_complete",
|
|
483
|
-
"Conflict clarification requested (relevant)",
|
|
484
|
-
{
|
|
485
|
-
requestId: reqId,
|
|
486
|
-
status: "info",
|
|
487
|
-
attributes: { conflictGate: "relevant" },
|
|
488
|
-
},
|
|
489
|
-
);
|
|
490
|
-
onEvent({ type: "message_complete", sessionId: ctx.conversationId });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const {
|
|
495
|
-
recall,
|
|
496
|
-
dynamicProfile,
|
|
497
|
-
softConflictInstruction,
|
|
498
|
-
recallInjectionStrategy,
|
|
499
|
-
} = memoryResult;
|
|
456
|
+
const { recall, dynamicProfile, recallInjectionStrategy } = memoryResult;
|
|
500
457
|
runMessages = memoryResult.runMessages;
|
|
501
458
|
|
|
502
459
|
// Build active surface context
|
|
@@ -585,7 +542,6 @@ export async function runAgentLoopImpl(
|
|
|
585
542
|
|
|
586
543
|
// Shared injection options — reused whenever we need to re-inject after reduction.
|
|
587
544
|
const injectionOpts = {
|
|
588
|
-
softConflictInstruction,
|
|
589
545
|
activeSurface,
|
|
590
546
|
workspaceTopLevelContext: ctx.workspaceTopLevelContext,
|
|
591
547
|
channelCapabilities: ctx.channelCapabilities ?? null,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conflict-gate logic extracted from Session.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Handles pending memory conflicts internally: dismisses non-user-evidenced
|
|
5
|
+
* and non-actionable conflicts, and attempts resolution when the user's reply
|
|
6
|
+
* looks like an explicit clarification with topical relevance. Never produces
|
|
7
|
+
* user-facing clarification text.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { resolveConflictClarification } from "../memory/clarification-resolver.js";
|
|
@@ -14,47 +16,34 @@ import {
|
|
|
14
16
|
} from "../memory/conflict-intent.js";
|
|
15
17
|
import {
|
|
16
18
|
isConflictKindPairEligible,
|
|
19
|
+
isConflictUserEvidenced,
|
|
17
20
|
isStatementConflictEligible,
|
|
18
21
|
} from "../memory/conflict-policy.js";
|
|
19
22
|
import type { PendingConflictDetail } from "../memory/conflict-store.js";
|
|
20
23
|
import {
|
|
21
24
|
applyConflictResolution,
|
|
22
25
|
listPendingConflictDetails,
|
|
23
|
-
markConflictAsked,
|
|
24
26
|
resolveConflict,
|
|
25
27
|
} from "../memory/conflict-store.js";
|
|
26
28
|
|
|
27
|
-
export interface ConflictGateDecision {
|
|
28
|
-
question: string;
|
|
29
|
-
relevant: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
29
|
export class ConflictGate {
|
|
33
|
-
private turnCounter = 0;
|
|
34
|
-
private lastAskedTurn = new Map<string, number>();
|
|
35
|
-
|
|
36
30
|
async evaluate(
|
|
37
31
|
userMessage: string,
|
|
38
32
|
conflictConfig: {
|
|
39
33
|
enabled: boolean;
|
|
40
34
|
gateMode: string;
|
|
41
35
|
relevanceThreshold: number;
|
|
42
|
-
reaskCooldownTurns: number;
|
|
43
36
|
resolverLlmTimeoutMs: number;
|
|
44
|
-
askOnIrrelevantTurns: boolean;
|
|
45
37
|
conflictableKinds: readonly string[];
|
|
46
38
|
},
|
|
47
39
|
scopeId = "default",
|
|
48
|
-
): Promise<
|
|
49
|
-
if (!conflictConfig.enabled || conflictConfig.gateMode !== "soft")
|
|
50
|
-
return null;
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
if (!conflictConfig.enabled || conflictConfig.gateMode !== "soft") return;
|
|
51
42
|
|
|
52
|
-
this.turnCounter += 1;
|
|
53
|
-
const threshold = conflictConfig.relevanceThreshold;
|
|
54
|
-
const cooldownTurns = Math.max(1, conflictConfig.reaskCooldownTurns);
|
|
55
43
|
const pendingBeforeResolve = listPendingConflictDetails(scopeId, 50);
|
|
56
44
|
|
|
57
|
-
// Dismiss non-actionable conflicts (kind/statement policy
|
|
45
|
+
// Dismiss non-actionable conflicts (kind/statement policy, incoherent pair,
|
|
46
|
+
// or assistant-inferred-only provenance with no user evidence)
|
|
58
47
|
const dismissedIds = new Set<string>();
|
|
59
48
|
for (const conflict of pendingBeforeResolve) {
|
|
60
49
|
const dismissReason = this.getDismissReason(
|
|
@@ -73,13 +62,15 @@ export class ConflictGate {
|
|
|
73
62
|
const actionablePending = pendingBeforeResolve.filter(
|
|
74
63
|
(c) => !dismissedIds.has(c.id),
|
|
75
64
|
);
|
|
65
|
+
|
|
66
|
+
// Attempt resolution only for explicit clarification-like replies with
|
|
67
|
+
// topical relevance to the conflict statements
|
|
76
68
|
const clarificationReply = looksLikeClarificationReply(userMessage);
|
|
77
69
|
const candidatesBeforeResolve = actionablePending.filter((conflict) => {
|
|
78
70
|
const relevance = computeConflictRelevance(userMessage, conflict);
|
|
79
71
|
return shouldAttemptConflictResolution({
|
|
80
72
|
clarificationReply,
|
|
81
73
|
relevance,
|
|
82
|
-
wasRecentlyAsked: this.wasRecentlyAsked(conflict.id, cooldownTurns),
|
|
83
74
|
});
|
|
84
75
|
});
|
|
85
76
|
await this.resolvePendingConflicts(
|
|
@@ -87,45 +78,6 @@ export class ConflictGate {
|
|
|
87
78
|
conflictConfig.resolverLlmTimeoutMs,
|
|
88
79
|
candidatesBeforeResolve,
|
|
89
80
|
);
|
|
90
|
-
|
|
91
|
-
const pending = listPendingConflictDetails(scopeId, 50);
|
|
92
|
-
if (pending.length === 0) return null;
|
|
93
|
-
|
|
94
|
-
const scored = pending.map((conflict) => ({
|
|
95
|
-
conflict,
|
|
96
|
-
relevance: computeConflictRelevance(userMessage, conflict),
|
|
97
|
-
}));
|
|
98
|
-
// Try relevant conflicts first
|
|
99
|
-
const askable = scored
|
|
100
|
-
.filter((entry) => entry.relevance >= threshold)
|
|
101
|
-
.find((entry) => this.shouldAsk(entry.conflict.id, cooldownTurns));
|
|
102
|
-
|
|
103
|
-
// If no relevant conflict to ask and askOnIrrelevantTurns is enabled, try ones
|
|
104
|
-
// below the threshold (including zero-relevance). Zero-relevance conflicts are
|
|
105
|
-
// surfaced but not tracked as asked, preventing wasRecentlyAsked from triggering
|
|
106
|
-
// heuristic resolution on subsequent unrelated turns.
|
|
107
|
-
const candidateToAsk =
|
|
108
|
-
askable ??
|
|
109
|
-
(conflictConfig.askOnIrrelevantTurns
|
|
110
|
-
? scored.find(
|
|
111
|
-
(entry) =>
|
|
112
|
-
entry.relevance < threshold &&
|
|
113
|
-
this.shouldAsk(entry.conflict.id, cooldownTurns),
|
|
114
|
-
)
|
|
115
|
-
: undefined);
|
|
116
|
-
|
|
117
|
-
if (!candidateToAsk) return null;
|
|
118
|
-
|
|
119
|
-
if (askable || candidateToAsk.relevance > 0) {
|
|
120
|
-
this.lastAskedTurn.set(candidateToAsk.conflict.id, this.turnCounter);
|
|
121
|
-
markConflictAsked(candidateToAsk.conflict.id);
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
question:
|
|
125
|
-
candidateToAsk.conflict.clarificationQuestion ??
|
|
126
|
-
buildFallbackConflictQuestion(candidateToAsk.conflict),
|
|
127
|
-
relevant: candidateToAsk.relevance >= threshold,
|
|
128
|
-
};
|
|
129
81
|
}
|
|
130
82
|
|
|
131
83
|
private async resolvePendingConflicts(
|
|
@@ -156,18 +108,6 @@ export class ConflictGate {
|
|
|
156
108
|
}
|
|
157
109
|
}
|
|
158
110
|
|
|
159
|
-
private shouldAsk(conflictId: string, cooldownTurns: number): boolean {
|
|
160
|
-
const lastAsked = this.lastAskedTurn.get(conflictId);
|
|
161
|
-
if (lastAsked === undefined) return true;
|
|
162
|
-
return this.turnCounter - lastAsked >= cooldownTurns;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private wasRecentlyAsked(conflictId: string, cooldownTurns: number): boolean {
|
|
166
|
-
const lastAsked = this.lastAskedTurn.get(conflictId);
|
|
167
|
-
if (lastAsked === undefined) return false;
|
|
168
|
-
return this.turnCounter - lastAsked <= cooldownTurns;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
111
|
/**
|
|
172
112
|
* Returns a dismissal reason if the conflict should be dismissed, or null if actionable.
|
|
173
113
|
*/
|
|
@@ -211,18 +151,17 @@ export class ConflictGate {
|
|
|
211
151
|
) {
|
|
212
152
|
return "Dismissed by conflict policy (incoherent — zero statement overlap).";
|
|
213
153
|
}
|
|
154
|
+
// Dismiss conflicts where neither side has user-evidenced provenance
|
|
155
|
+
if (
|
|
156
|
+
!isConflictUserEvidenced(
|
|
157
|
+
conflict.existingVerificationState,
|
|
158
|
+
conflict.candidateVerificationState,
|
|
159
|
+
)
|
|
160
|
+
) {
|
|
161
|
+
return "Dismissed by conflict policy (no user-evidenced provenance).";
|
|
162
|
+
}
|
|
214
163
|
return null;
|
|
215
164
|
}
|
|
216
165
|
}
|
|
217
166
|
|
|
218
|
-
export function buildFallbackConflictQuestion(
|
|
219
|
-
conflict: PendingConflictDetail,
|
|
220
|
-
): string {
|
|
221
|
-
return [
|
|
222
|
-
"I have two conflicting notes and need your confirmation.",
|
|
223
|
-
`A) ${conflict.existingStatement}`,
|
|
224
|
-
`B) ${conflict.candidateStatement}`,
|
|
225
|
-
"Which one should I keep?",
|
|
226
|
-
].join("\n");
|
|
227
|
-
}
|
|
228
167
|
export { computeConflictRelevance, looksLikeClarificationReply };
|
|
@@ -24,7 +24,6 @@ export interface MemoryRecallResult {
|
|
|
24
24
|
runMessages: Message[];
|
|
25
25
|
recall: Awaited<ReturnType<typeof buildMemoryRecall>>;
|
|
26
26
|
dynamicProfile: { text: string };
|
|
27
|
-
softConflictInstruction: string | null;
|
|
28
27
|
recallInjectionStrategy: RecallInjectionStrategy;
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -37,7 +36,7 @@ export interface MemoryPrepareContext {
|
|
|
37
36
|
scopeId: string;
|
|
38
37
|
includeDefaultFallback: boolean;
|
|
39
38
|
trustClass: "guardian" | "trusted_contact" | "unknown";
|
|
40
|
-
/** When false (e.g. scheduled tasks), skip conflict
|
|
39
|
+
/** When false (e.g. scheduled tasks), skip conflict gate evaluation. */
|
|
41
40
|
isInteractive?: boolean;
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -64,7 +63,7 @@ export async function prepareMemoryContext(
|
|
|
64
63
|
userMessageId: string,
|
|
65
64
|
abortSignal: AbortSignal,
|
|
66
65
|
onEvent: (msg: ServerMessage) => void,
|
|
67
|
-
): Promise<MemoryRecallResult
|
|
66
|
+
): Promise<MemoryRecallResult> {
|
|
68
67
|
// Provenance-based trust gating: untrusted actors skip all memory operations
|
|
69
68
|
// (recall, dynamic profile, conflict gate) to prevent untrusted content from
|
|
70
69
|
// influencing memory-augmented responses.
|
|
@@ -94,9 +93,7 @@ export async function prepareMemoryContext(
|
|
|
94
93
|
topCandidates: [],
|
|
95
94
|
} as Awaited<ReturnType<typeof buildMemoryRecall>>,
|
|
96
95
|
dynamicProfile: { text: "" },
|
|
97
|
-
softConflictInstruction: null,
|
|
98
96
|
recallInjectionStrategy: "prepend_user_block",
|
|
99
|
-
conflictClarification: null,
|
|
100
97
|
};
|
|
101
98
|
}
|
|
102
99
|
|
|
@@ -129,65 +126,25 @@ export async function prepareMemoryContext(
|
|
|
129
126
|
topCandidates: [],
|
|
130
127
|
} as Awaited<ReturnType<typeof buildMemoryRecall>>,
|
|
131
128
|
dynamicProfile: { text: "" },
|
|
132
|
-
softConflictInstruction: null,
|
|
133
129
|
recallInjectionStrategy: "prepend_user_block",
|
|
134
|
-
conflictClarification: null,
|
|
135
130
|
};
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
const runtimeConfig = getConfig();
|
|
139
134
|
const memoryEnabled = runtimeConfig.memory?.enabled !== false;
|
|
140
135
|
|
|
141
|
-
// Conflict gate —
|
|
142
|
-
//
|
|
136
|
+
// Conflict gate — evaluate for side effects (background resolution/dismissal)
|
|
137
|
+
// but do not return any user-facing payload. Non-interactive sessions skip
|
|
138
|
+
// entirely since there is no human context for conflict evaluation.
|
|
143
139
|
const isInteractive = ctx.isInteractive !== false;
|
|
144
140
|
const conflictConfig =
|
|
145
141
|
memoryEnabled && isInteractive
|
|
146
142
|
? runtimeConfig.memory?.conflicts
|
|
147
143
|
: undefined;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
: null;
|
|
151
|
-
|
|
152
|
-
if (conflictGateResult?.relevant) {
|
|
153
|
-
return {
|
|
154
|
-
runMessages: ctx.messages,
|
|
155
|
-
recall: {
|
|
156
|
-
enabled: false,
|
|
157
|
-
degraded: false,
|
|
158
|
-
injectedText: "",
|
|
159
|
-
lexicalHits: 0,
|
|
160
|
-
semanticHits: 0,
|
|
161
|
-
recencyHits: 0,
|
|
162
|
-
entityHits: 0,
|
|
163
|
-
relationSeedEntityCount: 0,
|
|
164
|
-
relationTraversedEdgeCount: 0,
|
|
165
|
-
relationNeighborEntityCount: 0,
|
|
166
|
-
relationExpandedItemCount: 0,
|
|
167
|
-
earlyTerminated: false,
|
|
168
|
-
mergedCount: 0,
|
|
169
|
-
selectedCount: 0,
|
|
170
|
-
rerankApplied: false,
|
|
171
|
-
injectedTokens: 0,
|
|
172
|
-
latencyMs: 0,
|
|
173
|
-
topCandidates: [],
|
|
174
|
-
} as Awaited<ReturnType<typeof buildMemoryRecall>>,
|
|
175
|
-
dynamicProfile: { text: "" },
|
|
176
|
-
softConflictInstruction: null,
|
|
177
|
-
recallInjectionStrategy: "prepend_user_block",
|
|
178
|
-
conflictClarification: [
|
|
179
|
-
conflictGateResult.question,
|
|
180
|
-
"",
|
|
181
|
-
"I need this clarification before I can give guidance that depends on that preference.",
|
|
182
|
-
].join("\n"),
|
|
183
|
-
};
|
|
144
|
+
if (conflictConfig) {
|
|
145
|
+
await ctx.conflictGate.evaluate(content, conflictConfig, ctx.scopeId);
|
|
184
146
|
}
|
|
185
147
|
|
|
186
|
-
const softConflictInstruction =
|
|
187
|
-
conflictGateResult && !conflictGateResult.relevant
|
|
188
|
-
? conflictGateResult.question
|
|
189
|
-
: null;
|
|
190
|
-
|
|
191
148
|
// Dynamic profile
|
|
192
149
|
const profileConfig = memoryEnabled
|
|
193
150
|
? runtimeConfig.memory?.profile
|
|
@@ -312,8 +269,6 @@ export async function prepareMemoryContext(
|
|
|
312
269
|
runMessages,
|
|
313
270
|
recall,
|
|
314
271
|
dynamicProfile,
|
|
315
|
-
softConflictInstruction,
|
|
316
272
|
recallInjectionStrategy,
|
|
317
|
-
conflictClarification: null,
|
|
318
273
|
};
|
|
319
274
|
}
|
|
@@ -541,7 +541,9 @@ export async function processMessage(
|
|
|
541
541
|
messageText: trimmedContent,
|
|
542
542
|
channel: "vellum",
|
|
543
543
|
actor: {
|
|
544
|
-
|
|
544
|
+
actorPrincipalId:
|
|
545
|
+
session.trustContext?.guardianPrincipalId ?? undefined,
|
|
546
|
+
actorExternalUserId: session.trustContext?.guardianExternalUserId,
|
|
545
547
|
channel: "vellum",
|
|
546
548
|
guardianPrincipalId:
|
|
547
549
|
session.trustContext?.guardianPrincipalId ?? undefined,
|
|
@@ -115,6 +115,10 @@ export interface InboundActorContext {
|
|
|
115
115
|
memberPolicy?: string;
|
|
116
116
|
/** Denial reason when access is blocked. */
|
|
117
117
|
denialReason?: string;
|
|
118
|
+
/** Free-text notes about this contact. */
|
|
119
|
+
contactNotes?: string;
|
|
120
|
+
/** Number of prior interactions with this contact. */
|
|
121
|
+
contactInteractionCount?: number;
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
/**
|
|
@@ -159,6 +163,9 @@ export function inboundActorContextFromTrust(
|
|
|
159
163
|
: undefined,
|
|
160
164
|
memberPolicy: ctx.memberRecord?.channel.policy ?? undefined,
|
|
161
165
|
denialReason: ctx.denialReason,
|
|
166
|
+
contactNotes: ctx.memberRecord?.contact.notes ?? undefined,
|
|
167
|
+
contactInteractionCount:
|
|
168
|
+
ctx.memberRecord?.contact.interactionCount ?? undefined,
|
|
162
169
|
};
|
|
163
170
|
}
|
|
164
171
|
|
|
@@ -392,24 +399,6 @@ export interface ActiveSurfaceContext {
|
|
|
392
399
|
appFiles?: string[];
|
|
393
400
|
}
|
|
394
401
|
|
|
395
|
-
/**
|
|
396
|
-
* Append a memory-conflict clarification instruction to the last user message.
|
|
397
|
-
*/
|
|
398
|
-
export function injectClarificationRequestIntoUserMessage(
|
|
399
|
-
message: Message,
|
|
400
|
-
question: string,
|
|
401
|
-
): Message {
|
|
402
|
-
const instruction = [
|
|
403
|
-
"[Memory clarification request]",
|
|
404
|
-
`Ask this once in your response: ${question}`,
|
|
405
|
-
"After asking, continue helping with the current request.",
|
|
406
|
-
].join("\n");
|
|
407
|
-
return {
|
|
408
|
-
...message,
|
|
409
|
-
content: [...message.content, { type: "text", text: `\n\n${instruction}` }],
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
402
|
const MAX_CONTEXT_LENGTH = 100_000;
|
|
414
403
|
|
|
415
404
|
function truncateHtml(html: string, budget: number): string {
|
|
@@ -791,6 +780,14 @@ export function buildInboundActorContextBlock(
|
|
|
791
780
|
lines.push(`member_policy: ${ctx.memberPolicy}`);
|
|
792
781
|
}
|
|
793
782
|
lines.push(`denial_reason: ${ctx.denialReason ?? "none"}`);
|
|
783
|
+
// Contact metadata — only included when the sender has a contact record
|
|
784
|
+
// with non-default values.
|
|
785
|
+
if (ctx.contactNotes) {
|
|
786
|
+
lines.push(`contact_notes: ${ctx.contactNotes}`);
|
|
787
|
+
}
|
|
788
|
+
if (ctx.contactInteractionCount != null && ctx.contactInteractionCount > 0) {
|
|
789
|
+
lines.push(`contact_interaction_count: ${ctx.contactInteractionCount}`);
|
|
790
|
+
}
|
|
794
791
|
if (
|
|
795
792
|
ctx.actorMemberDisplayName &&
|
|
796
793
|
ctx.actorSenderDisplayName &&
|
|
@@ -1050,9 +1047,9 @@ export function stripInjectedContext(
|
|
|
1050
1047
|
* - `'full'` (default): all injections are applied.
|
|
1051
1048
|
* - `'minimal'`: only safety-critical context is injected (channel turn,
|
|
1052
1049
|
* interface turn, inbound actor, non-interactive marker, voice call
|
|
1053
|
-
* control, channel capabilities
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1050
|
+
* control, channel capabilities). High-token optional blocks (workspace
|
|
1051
|
+
* top-level, temporal, channel command, active surface) are skipped to
|
|
1052
|
+
* reduce context pressure.
|
|
1056
1053
|
*/
|
|
1057
1054
|
export type InjectionMode = "full" | "minimal";
|
|
1058
1055
|
|
|
@@ -1065,7 +1062,6 @@ export type InjectionMode = "full" | "minimal";
|
|
|
1065
1062
|
export function applyRuntimeInjections(
|
|
1066
1063
|
runMessages: Message[],
|
|
1067
1064
|
options: {
|
|
1068
|
-
softConflictInstruction?: string | null;
|
|
1069
1065
|
activeSurface?: ActiveSurfaceContext | null;
|
|
1070
1066
|
workspaceTopLevelContext?: string | null;
|
|
1071
1067
|
channelCapabilities?: ChannelCapabilities | null;
|
|
@@ -1113,19 +1109,6 @@ export function applyRuntimeInjections(
|
|
|
1113
1109
|
}
|
|
1114
1110
|
}
|
|
1115
1111
|
|
|
1116
|
-
if (options.softConflictInstruction) {
|
|
1117
|
-
const userTail = result[result.length - 1];
|
|
1118
|
-
if (userTail && userTail.role === "user") {
|
|
1119
|
-
result = [
|
|
1120
|
-
...result.slice(0, -1),
|
|
1121
|
-
injectClarificationRequestIntoUserMessage(
|
|
1122
|
-
userTail,
|
|
1123
|
-
options.softConflictInstruction,
|
|
1124
|
-
),
|
|
1125
|
-
];
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
1112
|
if (mode === "full" && options.activeSurface) {
|
|
1130
1113
|
const userTail = result[result.length - 1];
|
|
1131
1114
|
if (userTail && userTail.role === "user") {
|
|
@@ -79,7 +79,11 @@ export class HeartbeatService {
|
|
|
79
79
|
|
|
80
80
|
// Active hours guard — only applied when both bounds are set.
|
|
81
81
|
// The schema rejects configs where only one bound is provided.
|
|
82
|
-
if (
|
|
82
|
+
if (
|
|
83
|
+
!force &&
|
|
84
|
+
config.activeHoursStart != null &&
|
|
85
|
+
config.activeHoursEnd != null
|
|
86
|
+
) {
|
|
83
87
|
const hour = this.deps.getCurrentHour?.() ?? new Date().getHours();
|
|
84
88
|
if (
|
|
85
89
|
!isWithinActiveHours(
|
|
@@ -193,16 +193,13 @@ export function looksLikeClarificationReply(userMessage: string): boolean {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Conflict resolution
|
|
197
|
-
*
|
|
198
|
-
* - a very recent explicit ask from the assistant.
|
|
196
|
+
* Conflict resolution requires explicit clarification intent with non-zero
|
|
197
|
+
* topical overlap with the conflict statements.
|
|
199
198
|
*/
|
|
200
199
|
export function shouldAttemptConflictResolution(input: {
|
|
201
200
|
clarificationReply: boolean;
|
|
202
201
|
relevance: number;
|
|
203
|
-
wasRecentlyAsked: boolean;
|
|
204
202
|
}): boolean {
|
|
205
203
|
if (!input.clarificationReply) return false;
|
|
206
|
-
|
|
207
|
-
return input.wasRecentlyAsked;
|
|
204
|
+
return input.relevance > 0;
|
|
208
205
|
}
|
|
@@ -71,6 +71,40 @@ export function isDurableInstructionStatement(statement: string): boolean {
|
|
|
71
71
|
return DURABLE_INSTRUCTION_CUES.test(statement);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// ── Verification-state provenance ──────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
// States indicating user involvement — either the user directly stated
|
|
77
|
+
// the information, explicitly confirmed it, or it was bulk-imported from
|
|
78
|
+
// a trusted external source the user chose to connect.
|
|
79
|
+
const USER_EVIDENCED_STATES = new Set([
|
|
80
|
+
"user_reported",
|
|
81
|
+
"user_confirmed",
|
|
82
|
+
"legacy_import",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns true when the verification state indicates user provenance
|
|
87
|
+
* (as opposed to purely assistant-inferred).
|
|
88
|
+
*/
|
|
89
|
+
export function isUserEvidencedVerificationState(state: string): boolean {
|
|
90
|
+
return USER_EVIDENCED_STATES.has(state);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns true when at least one side of a conflict pair has user-evidenced
|
|
95
|
+
* provenance. Assistant-inferred-only conflicts should not escalate into
|
|
96
|
+
* user-facing behavior.
|
|
97
|
+
*/
|
|
98
|
+
export function isConflictUserEvidenced(
|
|
99
|
+
existingState: string,
|
|
100
|
+
candidateState: string,
|
|
101
|
+
): boolean {
|
|
102
|
+
return (
|
|
103
|
+
isUserEvidencedVerificationState(existingState) ||
|
|
104
|
+
isUserEvidencedVerificationState(candidateState)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
/**
|
|
75
109
|
* Returns true when a statement of the given kind is eligible to participate
|
|
76
110
|
* in conflict detection at the statement level. This combines kind eligibility
|