@vellumai/assistant 0.3.27 → 0.4.0

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 (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -37,6 +37,7 @@ describe('handleUserMessage secret redirect continuation', () => {
37
37
  setTurnChannelContext: () => {},
38
38
  setTurnInterfaceContext: () => {},
39
39
  setAssistantId: () => {},
40
+ setChannelCapabilities: () => {},
40
41
  setGuardianContext: () => {},
41
42
  setCommandIntent: () => {},
42
43
  processMessage: (content: string, _attachments: unknown[], _onEvent: unknown, requestId: string) => {
@@ -12,7 +12,9 @@ let rawConfigStore: Record<string, unknown> = {};
12
12
  const saveRawConfigCalls: Record<string, unknown>[] = [];
13
13
 
14
14
  mock.module('../config/loader.js', () => ({
15
- getConfig: () => ({}),
15
+ getConfig: () => ({
16
+ ui: {},
17
+ }),
16
18
  loadConfig: () => ({}),
17
19
  loadRawConfig: () => ({ ...rawConfigStore }),
18
20
  saveRawConfig: (cfg: Record<string, unknown>) => {
@@ -11,7 +11,9 @@ const testDir = mkdtempSync(join(tmpdir(), 'handlers-telegram-cfg-test-'));
11
11
  let rawConfigStore: Record<string, unknown> = {};
12
12
 
13
13
  mock.module('../config/loader.js', () => ({
14
- getConfig: () => ({}),
14
+ getConfig: () => ({
15
+ ui: {},
16
+ }),
15
17
  loadConfig: () => ({}),
16
18
  loadRawConfig: () => ({ ...rawConfigStore }),
17
19
  saveRawConfig: (cfg: Record<string, unknown>) => {
@@ -11,7 +11,9 @@ const testDir = mkdtempSync(join(tmpdir(), 'handlers-twilio-cfg-test-'));
11
11
  let rawConfigStore: Record<string, unknown> = {};
12
12
 
13
13
  mock.module('../config/loader.js', () => ({
14
- getConfig: () => ({ ...rawConfigStore }),
14
+ getConfig: () => ({
15
+ ui: {},
16
+ ...rawConfigStore }),
15
17
  loadConfig: () => ({ ...rawConfigStore }),
16
18
  loadRawConfig: () => ({ ...rawConfigStore }),
17
19
  saveRawConfig: (cfg: Record<string, unknown>) => {
@@ -12,7 +12,9 @@ let rawConfigStore: Record<string, unknown> = {};
12
12
  const saveRawConfigCalls: Record<string, unknown>[] = [];
13
13
 
14
14
  mock.module('../config/loader.js', () => ({
15
- getConfig: () => ({}),
15
+ getConfig: () => ({
16
+ ui: {},
17
+ }),
16
18
  loadConfig: () => ({}),
17
19
  loadRawConfig: () => ({ ...rawConfigStore }),
18
20
  saveRawConfig: (cfg: Record<string, unknown>) => {
@@ -0,0 +1,318 @@
1
+ import * as net from 'node:net';
2
+
3
+ import { beforeEach, describe, expect, mock, test } from 'bun:test';
4
+
5
+ import type { HandlerContext } from '../daemon/handlers.js';
6
+ import type { UserMessage } from '../daemon/ipc-contract.js';
7
+ import type { ServerMessage } from '../daemon/ipc-protocol.js';
8
+ import { DebouncerMap } from '../util/debounce.js';
9
+
10
+ const routeGuardianReplyMock = mock(async () => ({
11
+ consumed: false,
12
+ decisionApplied: false,
13
+ type: 'not_consumed' as const,
14
+ })) as any;
15
+ const listPendingByDestinationMock = mock(() => [] as Array<{ id: string; kind?: string }>);
16
+ const listCanonicalMock = mock(() => [] as Array<{ id: string }>);
17
+ const getByConversationMock = mock(
18
+ () => [] as Array<{
19
+ requestId: string;
20
+ kind: 'confirmation' | 'secret';
21
+ session?: unknown;
22
+ }>,
23
+ );
24
+ const resolveMock = mock(() => undefined as unknown);
25
+ const addMessageMock = mock(async () => ({ id: 'persisted-message-id' }));
26
+ const getConfigMock = mock(() => ({
27
+ daemon: { standaloneRecording: false },
28
+ secretDetection: { customPatterns: [], entropyThreshold: 3.5 },
29
+ }));
30
+
31
+ mock.module('../runtime/guardian-reply-router.js', () => ({
32
+ routeGuardianReply: routeGuardianReplyMock,
33
+ }));
34
+
35
+ mock.module('../memory/canonical-guardian-store.js', () => ({
36
+ listPendingCanonicalGuardianRequestsByDestinationConversation: listPendingByDestinationMock,
37
+ listCanonicalGuardianRequests: listCanonicalMock,
38
+ }));
39
+
40
+ mock.module('../runtime/pending-interactions.js', () => ({
41
+ getByConversation: getByConversationMock,
42
+ resolve: resolveMock,
43
+ }));
44
+
45
+ mock.module('../memory/conversation-store.js', () => ({
46
+ addMessage: addMessageMock,
47
+ }));
48
+
49
+ mock.module('../config/loader.js', () => ({
50
+ getConfig: getConfigMock,
51
+ }));
52
+
53
+ mock.module('../daemon/approval-generators.js', () => ({
54
+ createApprovalConversationGenerator: () => async () => ({
55
+ disposition: 'keep_pending',
56
+ replyText: 'pending',
57
+ }),
58
+ }));
59
+
60
+ mock.module('../util/logger.js', () => ({
61
+ getLogger: () => ({
62
+ info: () => {},
63
+ warn: () => {},
64
+ error: () => {},
65
+ debug: () => {},
66
+ child: () => ({
67
+ info: () => {},
68
+ warn: () => {},
69
+ error: () => {},
70
+ debug: () => {},
71
+ }),
72
+ }),
73
+ }));
74
+
75
+ import { handleUserMessage } from '../daemon/handlers/sessions.js';
76
+
77
+ interface TestSession {
78
+ messages: Array<{ role: string; content: unknown[] }>;
79
+ hasEscalationHandler: () => boolean;
80
+ setChannelCapabilities: (caps: unknown) => void;
81
+ isProcessing: () => boolean;
82
+ hasPendingConfirmation: (requestId: string) => boolean;
83
+ hasAnyPendingConfirmation: () => boolean;
84
+ getQueueDepth: () => number;
85
+ denyAllPendingConfirmations: () => void;
86
+ enqueueMessage: (...args: unknown[]) => { queued: boolean; rejected?: boolean; requestId: string };
87
+ traceEmitter: { emit: (...args: unknown[]) => void };
88
+ setTurnChannelContext: (ctx: unknown) => void;
89
+ setTurnInterfaceContext: (ctx: unknown) => void;
90
+ setAssistantId: (assistantId: string) => void;
91
+ setGuardianContext: (ctx: unknown) => void;
92
+ setCommandIntent: (intent: unknown) => void;
93
+ processMessage: (...args: unknown[]) => Promise<string>;
94
+ }
95
+
96
+ function createContext(session: TestSession): { ctx: HandlerContext; sent: ServerMessage[] } {
97
+ const sent: ServerMessage[] = [];
98
+ const ctx: HandlerContext = {
99
+ sessions: new Map(),
100
+ socketToSession: new Map(),
101
+ cuSessions: new Map(),
102
+ socketToCuSession: new Map(),
103
+ cuObservationParseSequence: new Map(),
104
+ socketSandboxOverride: new Map(),
105
+ sharedRequestTimestamps: [],
106
+ debounceTimers: new DebouncerMap({ defaultDelayMs: 100 }),
107
+ suppressConfigReload: false,
108
+ setSuppressConfigReload: () => {},
109
+ updateConfigFingerprint: () => {},
110
+ send: (_socket, msg) => { sent.push(msg); },
111
+ broadcast: () => {},
112
+ clearAllSessions: () => 0,
113
+ getOrCreateSession: async () => session as any,
114
+ touchSession: () => {},
115
+ };
116
+ return { ctx, sent };
117
+ }
118
+
119
+ function makeMessage(content: string): UserMessage {
120
+ return {
121
+ type: 'user_message',
122
+ sessionId: 'conv-1',
123
+ content,
124
+ channel: 'vellum',
125
+ interface: 'macos',
126
+ };
127
+ }
128
+
129
+ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
130
+ return {
131
+ messages: [],
132
+ hasEscalationHandler: () => true,
133
+ setChannelCapabilities: () => {},
134
+ isProcessing: () => false,
135
+ hasPendingConfirmation: () => true,
136
+ hasAnyPendingConfirmation: () => true,
137
+ getQueueDepth: () => 0,
138
+ denyAllPendingConfirmations: mock(() => {}),
139
+ enqueueMessage: mock(() => ({ queued: true, requestId: 'queued-id' })),
140
+ traceEmitter: { emit: () => {} },
141
+ setTurnChannelContext: () => {},
142
+ setTurnInterfaceContext: () => {},
143
+ setAssistantId: () => {},
144
+ setGuardianContext: () => {},
145
+ setCommandIntent: () => {},
146
+ processMessage: async () => 'msg-id',
147
+ ...overrides,
148
+ };
149
+ }
150
+
151
+ describe('handleUserMessage pending-confirmation reply interception', () => {
152
+ beforeEach(() => {
153
+ routeGuardianReplyMock.mockClear();
154
+ listPendingByDestinationMock.mockClear();
155
+ listCanonicalMock.mockClear();
156
+ getByConversationMock.mockClear();
157
+ resolveMock.mockClear();
158
+ addMessageMock.mockClear();
159
+ getConfigMock.mockClear();
160
+ });
161
+
162
+ test('consumes decision replies before auto-deny', async () => {
163
+ listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
164
+ listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
165
+ routeGuardianReplyMock.mockResolvedValue({
166
+ consumed: true,
167
+ decisionApplied: true,
168
+ type: 'canonical_decision_applied',
169
+ requestId: 'req-1',
170
+ });
171
+
172
+ const session = makeSession();
173
+ const { ctx, sent } = createContext(session);
174
+
175
+ await handleUserMessage(makeMessage('go for it'), {} as net.Socket, ctx);
176
+
177
+ expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
178
+ const routeCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
179
+ expect(routeCall.messageText).toBe('go for it');
180
+ expect(typeof routeCall.approvalConversationGenerator).toBe('function');
181
+ expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(0);
182
+ expect((session.enqueueMessage as any).mock.calls.length).toBe(0);
183
+ expect(session.messages).toHaveLength(2);
184
+ expect(session.messages[0]?.role).toBe('user');
185
+ expect(session.messages[1]?.role).toBe('assistant');
186
+ expect(addMessageMock).toHaveBeenCalledTimes(2);
187
+ expect(addMessageMock).toHaveBeenCalledWith(
188
+ 'conv-1',
189
+ 'user',
190
+ expect.any(String),
191
+ expect.objectContaining({
192
+ userMessageChannel: 'vellum',
193
+ assistantMessageChannel: 'vellum',
194
+ userMessageInterface: 'macos',
195
+ assistantMessageInterface: 'macos',
196
+ provenanceActorRole: 'guardian',
197
+ }),
198
+ );
199
+ expect(addMessageMock).toHaveBeenCalledWith(
200
+ 'conv-1',
201
+ 'assistant',
202
+ expect.stringContaining('Decision applied.'),
203
+ expect.objectContaining({
204
+ userMessageChannel: 'vellum',
205
+ assistantMessageChannel: 'vellum',
206
+ userMessageInterface: 'macos',
207
+ assistantMessageInterface: 'macos',
208
+ provenanceActorRole: 'guardian',
209
+ }),
210
+ );
211
+ expect(sent.map((msg) => msg.type)).toEqual([
212
+ 'message_queued',
213
+ 'message_dequeued',
214
+ 'assistant_text_delta',
215
+ 'message_request_complete',
216
+ ]);
217
+ const assistantDelta = sent.find(
218
+ (msg): msg is Extract<ServerMessage, { type: 'assistant_text_delta' }> => msg.type === 'assistant_text_delta',
219
+ );
220
+ expect(assistantDelta?.text).toBe('Decision applied.');
221
+ const requestComplete = sent.find(
222
+ (msg): msg is Extract<ServerMessage, { type: 'message_request_complete' }> => msg.type === 'message_request_complete',
223
+ );
224
+ expect(requestComplete?.runStillActive).toBe(false);
225
+ });
226
+
227
+ test('does not mutate in-memory history while processing', async () => {
228
+ listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
229
+ listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
230
+ routeGuardianReplyMock.mockResolvedValue({
231
+ consumed: true,
232
+ decisionApplied: true,
233
+ type: 'canonical_decision_applied',
234
+ requestId: 'req-1',
235
+ });
236
+
237
+ const session = makeSession({ isProcessing: () => true });
238
+ const { ctx, sent } = createContext(session);
239
+
240
+ await handleUserMessage(makeMessage('approve'), {} as net.Socket, ctx);
241
+
242
+ expect(addMessageMock).toHaveBeenCalledTimes(2);
243
+ expect(session.messages).toHaveLength(0);
244
+ // assistant_text_delta must NOT be sent when the session is processing —
245
+ // it would contaminate the agent's in-flight streaming message on the client.
246
+ expect(sent.some((msg) => msg.type === 'assistant_text_delta')).toBe(false);
247
+ expect(sent.map((msg) => msg.type)).toEqual([
248
+ 'message_queued',
249
+ 'message_dequeued',
250
+ 'message_request_complete',
251
+ ]);
252
+ const requestComplete = sent.find(
253
+ (msg): msg is Extract<ServerMessage, { type: 'message_request_complete' }> => msg.type === 'message_request_complete',
254
+ );
255
+ expect(requestComplete?.runStillActive).toBe(true);
256
+ });
257
+
258
+ test('nl keep_pending falls back to existing auto-deny + queue behavior', async () => {
259
+ listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
260
+ listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
261
+ routeGuardianReplyMock.mockResolvedValue({
262
+ consumed: true,
263
+ decisionApplied: false,
264
+ type: 'nl_keep_pending',
265
+ requestId: 'req-1',
266
+ replyText: 'Need clarification',
267
+ });
268
+
269
+ const session = makeSession();
270
+ const { ctx, sent } = createContext(session);
271
+
272
+ await handleUserMessage(makeMessage('what does that do?'), {} as net.Socket, ctx);
273
+
274
+ expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
275
+ expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(1);
276
+ expect((session.enqueueMessage as any).mock.calls.length).toBe(1);
277
+ expect(session.messages).toHaveLength(0);
278
+ expect(addMessageMock).toHaveBeenCalledTimes(0);
279
+ expect(sent.some((msg) => msg.type === 'message_queued')).toBe(true);
280
+ expect(sent.some((msg) => msg.type === 'message_dequeued')).toBe(false);
281
+ });
282
+
283
+ test('routes only live pending confirmation request ids', async () => {
284
+ const session = makeSession({
285
+ hasPendingConfirmation: (requestId: string) => requestId === 'req-live',
286
+ });
287
+
288
+ getByConversationMock.mockReturnValue([
289
+ { requestId: 'req-stale', kind: 'confirmation', session: {} },
290
+ { requestId: 'req-live', kind: 'confirmation', session: session as unknown },
291
+ ]);
292
+ listPendingByDestinationMock.mockReturnValue([
293
+ { id: 'req-stale', kind: 'tool_approval' },
294
+ { id: 'req-live', kind: 'tool_approval' },
295
+ ]);
296
+ listCanonicalMock.mockReturnValue([
297
+ { id: 'req-stale' },
298
+ { id: 'req-live' },
299
+ ]);
300
+ routeGuardianReplyMock.mockResolvedValue({
301
+ consumed: false,
302
+ decisionApplied: false,
303
+ type: 'not_consumed',
304
+ });
305
+
306
+ const { ctx } = createContext(session);
307
+ await handleUserMessage(makeMessage('allow'), {} as net.Socket, ctx);
308
+
309
+ expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
310
+ const routeCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
311
+ expect(routeCall.pendingRequestIds).toEqual(['req-live']);
312
+ // Auto-deny clears matching confirmation entries from pending-interactions
313
+ // so stale IDs are not reused as routing candidates. Only the live
314
+ // session-scoped interaction should be resolved.
315
+ expect(resolveMock).toHaveBeenCalledTimes(1);
316
+ expect(resolveMock).toHaveBeenCalledWith('req-live');
317
+ });
318
+ });
@@ -31,6 +31,26 @@ const createdConversations: Array<{ title: string; threadType: string }> = [];
31
31
  let conversationIdCounter = 0;
32
32
 
33
33
  mock.module('../memory/conversation-store.js', () => ({
34
+ getConversationThreadType: () => 'default',
35
+ setConversationOriginChannelIfUnset: () => {},
36
+ updateConversationContextWindow: () => {},
37
+ deleteMessageById: () => {},
38
+ updateConversationTitle: () => {},
39
+ updateConversationUsage: () => {},
40
+ addMessage: () => ({ id: 'mock-msg-id' }),
41
+ getMessages: () => [],
42
+ getConversation: () => ({
43
+ id: 'conv-1',
44
+ contextSummary: null,
45
+ contextCompactedMessageCount: 0,
46
+ totalInputTokens: 0,
47
+ totalOutputTokens: 0,
48
+ totalEstimatedCost: 0,
49
+ title: null,
50
+ }),
51
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
52
+ getConversationOriginInterface: () => null,
53
+ getConversationOriginChannel: () => null,
34
54
  createConversation: (opts: { title: string; threadType: string }) => {
35
55
  createdConversations.push(opts);
36
56
  return { id: `conv-${++conversationIdCounter}`, ...opts };
@@ -364,4 +364,37 @@ describe('inbound invite redemption intercept', () => {
364
364
  expect(json.accepted).toBe(true);
365
365
  expect(json.denied).toBeUndefined();
366
366
  });
367
+
368
+ test('reactivation via invite preserves existing guardian-managed member display name', async () => {
369
+ const { rawToken } = createInvite({ sourceChannel: 'telegram', maxUses: 5 });
370
+
371
+ upsertMember({
372
+ assistantId: 'self',
373
+ sourceChannel: 'telegram',
374
+ externalUserId: 'user-invite-123',
375
+ externalChatId: 'chat-invite-test',
376
+ status: 'revoked',
377
+ policy: 'allow',
378
+ displayName: 'Jeff',
379
+ });
380
+
381
+ const req = buildInviteRequest(rawToken, {
382
+ senderName: 'Noa Flaherty',
383
+ });
384
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
385
+ const json = await resp.json() as Record<string, unknown>;
386
+
387
+ expect(json.accepted).toBe(true);
388
+ expect(json.inviteRedemption).toBe('redeemed');
389
+
390
+ const member = findMember({
391
+ assistantId: 'self',
392
+ sourceChannel: 'telegram',
393
+ externalUserId: 'user-invite-123',
394
+ externalChatId: 'chat-invite-test',
395
+ });
396
+ expect(member).not.toBeNull();
397
+ expect(member!.status).toBe('active');
398
+ expect(member!.displayName).toBe('Jeff');
399
+ });
367
400
  });
@@ -11,7 +11,9 @@ const testDir = mkdtempSync(join(tmpdir(), 'ingress-reconcile-test-'));
11
11
  let rawConfigStore: Record<string, unknown> = {};
12
12
 
13
13
  mock.module('../config/loader.js', () => ({
14
- getConfig: () => ({}),
14
+ getConfig: () => ({
15
+ ui: {},
16
+ }),
15
17
  loadConfig: () => ({}),
16
18
  loadRawConfig: () => ({ ...rawConfigStore }),
17
19
  saveRawConfig: (cfg: Record<string, unknown>) => {