@vellumai/assistant 0.4.29 → 0.4.31

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 (237) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/Dockerfile +14 -8
  3. package/README.md +7 -8
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +76 -43
  6. package/package.json +1 -1
  7. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  8. package/scripts/test.sh +1 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  10. package/src/__tests__/actor-token-service.test.ts +4 -3
  11. package/src/__tests__/app-executors.test.ts +7 -17
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  13. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  14. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  16. package/src/__tests__/channel-approval.test.ts +8 -0
  17. package/src/__tests__/channel-approvals.test.ts +39 -1
  18. package/src/__tests__/channel-guardian.test.ts +15 -5
  19. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  20. package/src/__tests__/config-schema.test.ts +0 -9
  21. package/src/__tests__/conflict-policy.test.ts +76 -0
  22. package/src/__tests__/conflict-store.test.ts +14 -20
  23. package/src/__tests__/contacts-tools.test.ts +8 -61
  24. package/src/__tests__/contradiction-checker.test.ts +5 -1
  25. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  26. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  27. package/src/__tests__/gemini-image-service.test.ts +2 -2
  28. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  29. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  30. package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
  31. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  33. package/src/__tests__/integrations-cli.test.ts +3 -27
  34. package/src/__tests__/intent-routing.test.ts +3 -0
  35. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  36. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  37. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
  39. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  40. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  41. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  42. package/src/__tests__/registry.test.ts +0 -10
  43. package/src/__tests__/relay-server.test.ts +1 -1
  44. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  45. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  46. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  47. package/src/__tests__/session-agent-loop.test.ts +0 -2
  48. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  49. package/src/__tests__/session-media-retry.test.ts +147 -0
  50. package/src/__tests__/session-profile-injection.test.ts +0 -2
  51. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  52. package/src/__tests__/session-skill-tools.test.ts +0 -49
  53. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  54. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  55. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  57. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  58. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  59. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  60. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  61. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  62. package/src/__tests__/slack-skill.test.ts +3 -2
  63. package/src/__tests__/starter-task-flow.test.ts +0 -1
  64. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  65. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  66. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  68. package/src/amazon/client.ts +7 -24
  69. package/src/approvals/guardian-decision-primitive.ts +11 -7
  70. package/src/approvals/guardian-request-resolvers.ts +5 -3
  71. package/src/calls/relay-server.ts +44 -11
  72. package/src/channels/config.ts +1 -1
  73. package/src/cli/integrations.ts +10 -66
  74. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  75. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  76. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  77. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  78. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  79. package/src/config/bundled-skills/contacts/SKILL.md +49 -53
  80. package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
  81. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
  82. package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
  83. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
  84. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  85. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  86. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  87. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  88. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  89. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  90. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  91. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  92. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  93. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  94. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  95. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  96. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  97. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  98. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  99. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  100. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  101. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  102. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  103. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  104. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  105. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  106. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  107. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  108. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  109. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  110. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  111. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  112. package/src/config/bundled-tool-registry.ts +2 -5
  113. package/src/config/channel-permission-profiles.ts +155 -0
  114. package/src/config/env.ts +4 -1
  115. package/src/config/memory-schema.ts +0 -10
  116. package/src/config/system-prompt.ts +6 -0
  117. package/src/contacts/contact-store.ts +221 -56
  118. package/src/contacts/contacts-write.ts +14 -3
  119. package/src/contacts/types.ts +35 -4
  120. package/src/daemon/assistant-attachments.ts +23 -3
  121. package/src/daemon/guardian-verification-intent.ts +7 -4
  122. package/src/daemon/handlers/apps.ts +1 -2
  123. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  124. package/src/daemon/handlers/config-inbox.ts +16 -134
  125. package/src/daemon/handlers/contacts.ts +2 -2
  126. package/src/daemon/handlers/guardian-actions.ts +21 -88
  127. package/src/daemon/handlers/sessions.ts +2 -2
  128. package/src/daemon/ipc-contract/apps.ts +0 -1
  129. package/src/daemon/ipc-contract/contacts.ts +2 -2
  130. package/src/daemon/ipc-contract/inbox.ts +7 -66
  131. package/src/daemon/ipc-contract/sessions.ts +1 -0
  132. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  133. package/src/daemon/ipc-contract-inventory.json +2 -4
  134. package/src/daemon/lifecycle.ts +14 -2
  135. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  136. package/src/daemon/session-agent-loop.ts +2 -45
  137. package/src/daemon/session-attachments.ts +5 -1
  138. package/src/daemon/session-conflict-gate.ts +21 -82
  139. package/src/daemon/session-error.ts +18 -0
  140. package/src/daemon/session-lifecycle.ts +4 -5
  141. package/src/daemon/session-media-retry.ts +15 -1
  142. package/src/daemon/session-memory.ts +7 -52
  143. package/src/daemon/session-process.ts +3 -1
  144. package/src/daemon/session-runtime-assembly.ts +18 -35
  145. package/src/daemon/session-surfaces.ts +0 -1
  146. package/src/daemon/session-tool-setup.ts +7 -4
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/heartbeat/heartbeat-service.ts +5 -1
  149. package/src/home-base/prebuilt/seed.ts +0 -1
  150. package/src/influencer/client.ts +7 -24
  151. package/src/media/gemini-image-service.ts +48 -3
  152. package/src/memory/app-store.ts +0 -4
  153. package/src/memory/conflict-intent.ts +3 -6
  154. package/src/memory/conflict-policy.ts +34 -0
  155. package/src/memory/conflict-store.ts +10 -18
  156. package/src/memory/contradiction-checker.ts +2 -2
  157. package/src/memory/conversation-attention-store.ts +3 -1
  158. package/src/memory/db-init.ts +8 -0
  159. package/src/memory/job-handlers/conflict.ts +0 -7
  160. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  161. package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
  162. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  163. package/src/memory/migrations/index.ts +3 -0
  164. package/src/memory/schema.ts +12 -17
  165. package/src/memory/slack-thread-store.ts +187 -0
  166. package/src/messaging/index.ts +0 -1
  167. package/src/messaging/providers/slack/client.ts +84 -26
  168. package/src/messaging/providers/slack/types.ts +4 -0
  169. package/src/messaging/types.ts +0 -38
  170. package/src/notifications/adapters/slack.ts +90 -0
  171. package/src/notifications/destination-resolver.ts +42 -1
  172. package/src/notifications/emit-signal.ts +17 -1
  173. package/src/oauth/provider-profiles.ts +22 -0
  174. package/src/providers/anthropic/client.ts +3 -0
  175. package/src/providers/openai/client.ts +3 -0
  176. package/src/providers/retry.ts +9 -1
  177. package/src/runtime/actor-trust-resolver.ts +8 -0
  178. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  179. package/src/runtime/auth/route-policy.ts +4 -8
  180. package/src/runtime/channel-approval-types.ts +18 -0
  181. package/src/runtime/channel-approvals.ts +8 -0
  182. package/src/runtime/channel-invite-transport.ts +1 -1
  183. package/src/runtime/channel-reply-delivery.ts +62 -3
  184. package/src/runtime/gateway-client.ts +36 -2
  185. package/src/runtime/gateway-internal-client.ts +86 -0
  186. package/src/runtime/guardian-action-service.ts +128 -0
  187. package/src/runtime/guardian-outbound-actions.ts +3 -3
  188. package/src/runtime/guardian-reply-router.ts +4 -4
  189. package/src/runtime/guardian-verification-templates.ts +16 -1
  190. package/src/runtime/http-server.ts +29 -46
  191. package/src/runtime/invite-redemption-service.ts +1 -1
  192. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  193. package/src/runtime/nl-approval-parser.ts +138 -0
  194. package/src/runtime/routes/approval-routes.ts +1 -40
  195. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  196. package/src/runtime/routes/channel-route-shared.ts +35 -1
  197. package/src/runtime/routes/contact-routes.ts +494 -47
  198. package/src/runtime/routes/conversation-routes.ts +2 -1
  199. package/src/runtime/routes/global-search-routes.ts +2 -2
  200. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  201. package/src/runtime/routes/guardian-approval-interception.ts +78 -1
  202. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
  203. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  204. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
  205. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  206. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  207. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  208. package/src/runtime/routes/migration-routes.ts +17 -17
  209. package/src/runtime/slack-block-formatting.ts +176 -0
  210. package/src/schedule/scheduler.ts +11 -2
  211. package/src/tools/apps/executors.ts +16 -15
  212. package/src/tools/calls/call-end.ts +1 -1
  213. package/src/tools/computer-use/definitions.ts +16 -0
  214. package/src/tools/credentials/vault.ts +86 -2
  215. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  216. package/src/tools/permission-checker.ts +18 -0
  217. package/src/tools/terminal/shell.ts +15 -5
  218. package/src/tools/tool-approval-handler.ts +48 -4
  219. package/src/tools/types.ts +38 -1
  220. package/src/util/errors.ts +5 -1
  221. package/src/util/retry.ts +21 -0
  222. package/src/watcher/providers/slack.ts +33 -3
  223. package/src/workspace/git-service.ts +6 -4
  224. package/src/__tests__/get-weather.test.ts +0 -393
  225. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  226. package/src/autonomy/autonomy-resolver.ts +0 -62
  227. package/src/autonomy/autonomy-store.ts +0 -138
  228. package/src/autonomy/disposition-mapper.ts +0 -31
  229. package/src/autonomy/index.ts +0 -11
  230. package/src/autonomy/types.ts +0 -43
  231. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  232. package/src/config/bundled-skills/weather/TOOLS.json +0 -32
  233. package/src/config/bundled-skills/weather/icon.svg +0 -24
  234. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  235. package/src/messaging/triage-engine.ts +0 -344
  236. package/src/tools/weather/service.ts +0 -712
  237. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -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,
@@ -1238,6 +1194,7 @@ export async function runAgentLoopImpl(
1238
1194
  ctx.hasNoClient,
1239
1195
  ),
1240
1196
  state.lastAssistantMessageId,
1197
+ state.toolContentBlockToolNames,
1241
1198
  );
1242
1199
  const { assistantAttachments, emittedAttachments } = attachmentResult;
1243
1200
 
@@ -124,6 +124,7 @@ export async function resolveAssistantAttachments(
124
124
  workingDir: string,
125
125
  approveHostRead: ApproveHostRead,
126
126
  lastAssistantMessageId: string | undefined,
127
+ toolContentBlockToolNames?: ReadonlyMap<number, string>,
127
128
  ): Promise<AttachmentResolutionResult> {
128
129
  let assistantAttachments: AssistantAttachmentDraft[] = [];
129
130
  const emittedAttachments: UserMessageAttachment[] = [];
@@ -170,7 +171,10 @@ export async function resolveAssistantAttachments(
170
171
  "Directive resolution complete",
171
172
  );
172
173
 
173
- const toolDrafts = contentBlocksToDrafts(accumulatedToolContentBlocks);
174
+ const toolDrafts = contentBlocksToDrafts(
175
+ accumulatedToolContentBlocks,
176
+ toolContentBlockToolNames,
177
+ );
174
178
  // Most recent tool outputs (e.g., final browser screenshot) should win
175
179
  // the MAX_ASSISTANT_ATTACHMENTS cap over older intermediate screenshots.
176
180
  toolDrafts.reverse();
@@ -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 };
@@ -188,6 +188,24 @@ function classifyCore(
188
188
  retryable: false,
189
189
  };
190
190
  }
191
+ if (/credit balance is too low|insufficient.*credits?/i.test(message)) {
192
+ return {
193
+ code: "PROVIDER_BILLING",
194
+ userMessage: "Your API key has insufficient credits.",
195
+ retryable: false,
196
+ };
197
+ }
198
+ if (
199
+ /invalid.*api.?key|invalid.*x-api-key|authentication.?error|invalid.authentication/i.test(
200
+ message,
201
+ )
202
+ ) {
203
+ return {
204
+ code: "PROVIDER_BILLING",
205
+ userMessage: "Your API key is invalid.",
206
+ retryable: false,
207
+ };
208
+ }
191
209
  return {
192
210
  code: "PROVIDER_API",
193
211
  userMessage: "The AI provider rejected the request.",
@@ -13,7 +13,10 @@ import * as conversationStore from "../memory/conversation-store.js";
13
13
  import type { PermissionPrompter } from "../permissions/prompter.js";
14
14
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
15
15
  import type { ContentBlock, Message } from "../providers/types.js";
16
- import type { TrustClass } from "../runtime/actor-trust-resolver.js";
16
+ import {
17
+ isUntrustedTrustClass,
18
+ type TrustClass,
19
+ } from "../runtime/actor-trust-resolver.js";
17
20
  import { unregisterSessionSender } from "../tools/browser/browser-screencast.js";
18
21
  import { getLogger } from "../util/logger.js";
19
22
  import { repairHistory } from "./history-repair.js";
@@ -47,10 +50,6 @@ function parseProvenanceTrustClass(
47
50
  return undefined;
48
51
  }
49
52
 
50
- function isUntrustedTrustClass(trustClass: TrustClass | undefined): boolean {
51
- return trustClass === "trusted_contact" || trustClass === "unknown";
52
- }
53
-
54
53
  function filterMessagesForUntrustedActor(
55
54
  messages: conversationStore.MessageRow[],
56
55
  ): conversationStore.MessageRow[] {
@@ -19,15 +19,29 @@ export function stripMediaPayloadsForRetry(messages: Message[]): {
19
19
  latestUserIndex: number | null;
20
20
  } {
21
21
  let latestUserIndex: number | null = null;
22
+ let lastSummaryUserIndex: number | null = null;
22
23
  for (let i = messages.length - 1; i >= 0; i--) {
23
24
  const msg = messages[i];
24
25
  if (msg.role !== "user") continue;
25
- if (getSummaryFromContextMessage(msg) != null) continue;
26
26
  if (isToolResultOnlyMessage(msg)) continue;
27
+ if (getSummaryFromContextMessage(msg) != null) {
28
+ // Track the last summary message as a fallback — after aggressive
29
+ // compaction (minKeepRecentUserTurns: 0), the summary may be the only
30
+ // user message left and it can contain preserved image blocks that
31
+ // should not be stripped.
32
+ if (lastSummaryUserIndex == null) lastSummaryUserIndex = i;
33
+ continue;
34
+ }
27
35
  latestUserIndex = i;
28
36
  break;
29
37
  }
30
38
 
39
+ // Fall back to the summary message when compaction consumed all user turns,
40
+ // so images preserved by compaction are not unconditionally stripped.
41
+ if (latestUserIndex == null && lastSummaryUserIndex != null) {
42
+ latestUserIndex = lastSummaryUserIndex;
43
+ }
44
+
31
45
  let modified = false;
32
46
  let replacedBlocks = 0;
33
47
  let keptLatestMediaBlocks = 0;
@@ -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") {
@@ -1119,7 +1119,6 @@ export async function surfaceProxyResolver(
1119
1119
  const surfaceData: DynamicPageSurfaceData = {
1120
1120
  html: app.htmlDefinition,
1121
1121
  appId: app.id,
1122
- appType: app.appType,
1123
1122
  preview: {
1124
1123
  ...defaultPreview,
1125
1124
  ...preview,
@@ -7,10 +7,7 @@
7
7
  */
8
8
 
9
9
  import { isHttpAuthDisabled } from "../config/env.js";
10
- import type {
11
- ProxyApprovalCallback,
12
- ProxyApprovalRequest,
13
- } from "../outbound-proxy/index.js";
10
+ import { getBindingByConversation } from "../memory/external-conversation-store.js";
14
11
  import {
15
12
  generateAllowlistOptions,
16
13
  generateScopeOptions,
@@ -32,6 +29,8 @@ import { requestComputerControlTool } from "../tools/computer-use/request-comput
32
29
  import type { ToolExecutor } from "../tools/executor.js";
33
30
  import { getAllToolDefinitions } from "../tools/registry.js";
34
31
  import type {
32
+ ProxyApprovalCallback,
33
+ ProxyApprovalRequest,
35
34
  ToolExecutionResult,
36
35
  ToolLifecycleEventHandler,
37
36
  } from "../tools/types.js";
@@ -163,6 +162,10 @@ export function createToolExecutor(
163
162
  ctx.surfaceActionRequestIds?.has(ctx.currentRequestId ?? "") ?? false,
164
163
  requesterExternalUserId: ctx.trustContext?.requesterExternalUserId,
165
164
  requesterChatId: ctx.trustContext?.requesterChatId,
165
+ channelPermissionChannelId:
166
+ ctx.trustContext?.sourceChannel === "slack"
167
+ ? getBindingByConversation(ctx.conversationId)?.externalChatId
168
+ : undefined,
166
169
  onOutput,
167
170
  signal: ctx.abortController?.signal,
168
171
  sandboxOverride: ctx.sandboxOverride,
@@ -26,7 +26,8 @@ export interface ToolDomainEvents {
26
26
  | "allow_thread"
27
27
  | "always_allow"
28
28
  | "deny"
29
- | "always_deny";
29
+ | "always_deny"
30
+ | "temporary_override";
30
31
  riskLevel: string;
31
32
  decidedAtMs: number;
32
33
  };
@@ -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(
@@ -115,7 +115,6 @@ export function ensurePrebuiltHomeBaseSeeded(): {
115
115
  description: buildDescription(metadata),
116
116
  schemaJson: "{}",
117
117
  htmlDefinition: html,
118
- appType: "app",
119
118
  });
120
119
 
121
120
  log.info({ appId: created.id }, "Seeded prebuilt Home Base app");