@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.
Files changed (194) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +14 -8
  3. package/README.md +2 -2
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +1 -4
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  8. package/src/__tests__/anthropic-provider.test.ts +86 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/checker.test.ts +37 -98
  11. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
  12. package/src/__tests__/config-schema.test.ts +6 -14
  13. package/src/__tests__/conflict-policy.test.ts +76 -0
  14. package/src/__tests__/conflict-store.test.ts +14 -20
  15. package/src/__tests__/contacts-tools.test.ts +8 -61
  16. package/src/__tests__/contradiction-checker.test.ts +5 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  19. package/src/__tests__/followup-tools.test.ts +0 -30
  20. package/src/__tests__/gemini-provider.test.ts +79 -1
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  22. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  23. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  24. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  25. package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
  26. package/src/__tests__/memory-regressions.test.ts +6 -6
  27. package/src/__tests__/openai-provider.test.ts +82 -0
  28. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  29. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  30. package/src/__tests__/recurrence-types.test.ts +0 -15
  31. package/src/__tests__/registry.test.ts +0 -10
  32. package/src/__tests__/schedule-tools.test.ts +28 -44
  33. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  34. package/src/__tests__/session-agent-loop.test.ts +0 -2
  35. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  36. package/src/__tests__/session-profile-injection.test.ts +0 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  38. package/src/__tests__/session-skill-tools.test.ts +0 -49
  39. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  40. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  41. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  42. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  43. package/src/__tests__/task-management-tools.test.ts +111 -0
  44. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  45. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  46. package/src/__tests__/twilio-config.test.ts +0 -3
  47. package/src/amazon/session.ts +30 -91
  48. package/src/approvals/guardian-decision-primitive.ts +11 -7
  49. package/src/approvals/guardian-request-resolvers.ts +5 -3
  50. package/src/calls/call-controller.ts +423 -571
  51. package/src/calls/finalize-call.ts +20 -0
  52. package/src/calls/relay-access-wait.ts +340 -0
  53. package/src/calls/relay-server.ts +269 -899
  54. package/src/calls/relay-setup-router.ts +307 -0
  55. package/src/calls/relay-verification.ts +280 -0
  56. package/src/calls/twilio-config.ts +1 -8
  57. package/src/calls/voice-control-protocol.ts +184 -0
  58. package/src/calls/voice-session-bridge.ts +1 -8
  59. package/src/config/agent-schema.ts +1 -1
  60. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  61. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  62. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  63. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  64. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  65. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  66. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  67. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  68. package/src/config/bundled-tool-registry.ts +0 -5
  69. package/src/config/core-schema.ts +1 -1
  70. package/src/config/env.ts +0 -10
  71. package/src/config/feature-flag-registry.json +1 -1
  72. package/src/config/loader.ts +19 -0
  73. package/src/config/memory-schema.ts +0 -10
  74. package/src/config/schema.ts +2 -2
  75. package/src/config/system-prompt.ts +6 -0
  76. package/src/contacts/contact-store.ts +36 -62
  77. package/src/contacts/contacts-write.ts +14 -3
  78. package/src/contacts/types.ts +9 -4
  79. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  80. package/src/daemon/handlers/contacts.ts +2 -2
  81. package/src/daemon/handlers/guardian-actions.ts +1 -1
  82. package/src/daemon/handlers/session-history.ts +398 -0
  83. package/src/daemon/handlers/session-user-message.ts +982 -0
  84. package/src/daemon/handlers/sessions.ts +9 -1337
  85. package/src/daemon/ipc-contract/contacts.ts +2 -2
  86. package/src/daemon/ipc-contract/sessions.ts +0 -6
  87. package/src/daemon/ipc-contract-inventory.json +0 -1
  88. package/src/daemon/lifecycle.ts +0 -29
  89. package/src/daemon/session-agent-loop.ts +1 -45
  90. package/src/daemon/session-conflict-gate.ts +21 -82
  91. package/src/daemon/session-memory.ts +7 -52
  92. package/src/daemon/session-process.ts +3 -1
  93. package/src/daemon/session-runtime-assembly.ts +18 -35
  94. package/src/heartbeat/heartbeat-service.ts +5 -1
  95. package/src/home-base/app-link-store.ts +0 -7
  96. package/src/memory/conflict-intent.ts +3 -6
  97. package/src/memory/conflict-policy.ts +34 -0
  98. package/src/memory/conflict-store.ts +10 -18
  99. package/src/memory/contradiction-checker.ts +2 -2
  100. package/src/memory/conversation-attention-store.ts +1 -1
  101. package/src/memory/conversation-store.ts +0 -51
  102. package/src/memory/db-init.ts +8 -0
  103. package/src/memory/job-handlers/conflict.ts +24 -7
  104. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  105. package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
  106. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  107. package/src/memory/migrations/index.ts +2 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/recall-cache.ts +0 -5
  110. package/src/memory/schema/calls.ts +274 -0
  111. package/src/memory/schema/contacts.ts +125 -0
  112. package/src/memory/schema/conversations.ts +129 -0
  113. package/src/memory/schema/guardian.ts +172 -0
  114. package/src/memory/schema/index.ts +8 -0
  115. package/src/memory/schema/infrastructure.ts +205 -0
  116. package/src/memory/schema/memory-core.ts +196 -0
  117. package/src/memory/schema/notifications.ts +191 -0
  118. package/src/memory/schema/tasks.ts +78 -0
  119. package/src/memory/schema.ts +1 -1402
  120. package/src/memory/slack-thread-store.ts +0 -69
  121. package/src/messaging/index.ts +0 -1
  122. package/src/messaging/types.ts +0 -38
  123. package/src/notifications/decisions-store.ts +2 -105
  124. package/src/notifications/deliveries-store.ts +0 -11
  125. package/src/notifications/preferences-store.ts +1 -58
  126. package/src/permissions/checker.ts +6 -17
  127. package/src/providers/anthropic/client.ts +6 -2
  128. package/src/providers/gemini/client.ts +13 -2
  129. package/src/providers/managed-proxy/constants.ts +55 -0
  130. package/src/providers/managed-proxy/context.ts +77 -0
  131. package/src/providers/registry.ts +112 -0
  132. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  133. package/src/runtime/guardian-action-service.ts +3 -2
  134. package/src/runtime/guardian-outbound-actions.ts +3 -3
  135. package/src/runtime/guardian-reply-router.ts +4 -4
  136. package/src/runtime/http-server.ts +83 -710
  137. package/src/runtime/http-types.ts +0 -16
  138. package/src/runtime/middleware/auth.ts +0 -12
  139. package/src/runtime/routes/app-routes.ts +33 -0
  140. package/src/runtime/routes/approval-routes.ts +32 -0
  141. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  142. package/src/runtime/routes/attachment-routes.ts +32 -0
  143. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  144. package/src/runtime/routes/call-routes.ts +41 -0
  145. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  146. package/src/runtime/routes/channel-routes.ts +70 -0
  147. package/src/runtime/routes/contact-routes.ts +371 -29
  148. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  149. package/src/runtime/routes/conversation-routes.ts +192 -194
  150. package/src/runtime/routes/debug-routes.ts +15 -0
  151. package/src/runtime/routes/events-routes.ts +16 -0
  152. package/src/runtime/routes/global-search-routes.ts +17 -2
  153. package/src/runtime/routes/guardian-action-routes.ts +23 -1
  154. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  155. package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
  156. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  157. package/src/runtime/routes/identity-routes.ts +20 -0
  158. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  159. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  160. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  161. package/src/runtime/routes/integration-routes.ts +83 -0
  162. package/src/runtime/routes/invite-routes.ts +31 -0
  163. package/src/runtime/routes/migration-routes.ts +47 -17
  164. package/src/runtime/routes/pairing-routes.ts +18 -0
  165. package/src/runtime/routes/secret-routes.ts +20 -0
  166. package/src/runtime/routes/surface-action-routes.ts +26 -0
  167. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  168. package/src/runtime/routes/twilio-routes.ts +79 -0
  169. package/src/schedule/recurrence-types.ts +1 -11
  170. package/src/tools/followups/followup_create.ts +9 -3
  171. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  172. package/src/tools/memory/definitions.ts +0 -6
  173. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  174. package/src/tools/schedule/create.ts +1 -3
  175. package/src/tools/schedule/update.ts +9 -6
  176. package/src/twitter/session.ts +29 -77
  177. package/src/util/cookie-session.ts +114 -0
  178. package/src/workspace/git-service.ts +6 -4
  179. package/src/__tests__/conversation-routes.test.ts +0 -99
  180. package/src/__tests__/get-weather.test.ts +0 -393
  181. package/src/__tests__/task-tools.test.ts +0 -685
  182. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  183. package/src/autonomy/autonomy-resolver.ts +0 -62
  184. package/src/autonomy/autonomy-store.ts +0 -138
  185. package/src/autonomy/disposition-mapper.ts +0 -31
  186. package/src/autonomy/index.ts +0 -11
  187. package/src/autonomy/types.ts +0 -43
  188. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  189. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  190. package/src/config/bundled-skills/weather/icon.svg +0 -24
  191. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  192. package/src/contacts/startup-migration.ts +0 -21
  193. package/src/messaging/triage-engine.ts +0 -344
  194. 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
- relationship?: string;
44
- importance: number;
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
@@ -123,7 +123,6 @@
123
123
  "reorder_threads",
124
124
  "ride_shotgun_start",
125
125
  "ride_shotgun_stop",
126
- "sandbox_set",
127
126
  "schedule_remove",
128
127
  "schedule_run_now",
129
128
  "schedule_toggle",
@@ -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
- if (memoryResult.conflictClarification) {
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
- * Decides whether to ask the user about a pending memory conflict (relevant gate)
5
- * or skip entirely.
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<ConflictGateDecision | null> {
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 or incoherent pair)
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 clarification prompts. */
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 & { conflictClarification: string | null }> {
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 — skip entirely for non-interactive sessions (scheduled tasks,
142
- // work items) since there is no human to answer the clarification question.
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
- const conflictGateResult = conflictConfig
149
- ? await ctx.conflictGate.evaluate(content, conflictConfig, ctx.scopeId)
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
- externalUserId: session.trustContext?.guardianExternalUserId,
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, soft conflict). High-token optional
1054
- * blocks (workspace top-level, temporal, channel command, active surface)
1055
- * are skipped to reduce context pressure.
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 (!force && config.activeHoursStart != null && config.activeHoursEnd != null) {
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(
@@ -76,10 +76,3 @@ export function setHomeBaseAppLink(
76
76
  updatedAt: now,
77
77
  };
78
78
  }
79
-
80
- export function clearHomeBaseAppLink(): void {
81
- const db = getDb();
82
- db.delete(homeBaseAppLinks)
83
- .where(eq(homeBaseAppLinks.id, HOME_BASE_LINK_ID))
84
- .run();
85
- }
@@ -193,16 +193,13 @@ export function looksLikeClarificationReply(userMessage: string): boolean {
193
193
  }
194
194
 
195
195
  /**
196
- * Conflict resolution should require explicit clarification intent and either:
197
- * - non-zero topical overlap with the conflict statements, or
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
- if (input.relevance > 0) return true;
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