@vellumai/assistant 0.11.10 → 0.11.11-staging.1

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 (199) hide show
  1. package/AGENTS.md +1 -1
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  6. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  12. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  13. package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  14. package/openapi.yaml +152 -55
  15. package/package.json +1 -1
  16. package/src/__tests__/anthropic-provider.test.ts +60 -0
  17. package/src/__tests__/config-get-vision-flag.test.ts +36 -0
  18. package/src/__tests__/conversation-error.test.ts +38 -0
  19. package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
  20. package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
  21. package/src/__tests__/headless-browser-mode.test.ts +63 -13
  22. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  23. package/src/__tests__/heartbeat-service.test.ts +118 -27
  24. package/src/__tests__/host-app-control-proxy.test.ts +2 -2
  25. package/src/__tests__/host-app-control-routes.test.ts +41 -0
  26. package/src/__tests__/host-bash-routes.test.ts +30 -1
  27. package/src/__tests__/host-browser-routes.test.ts +34 -0
  28. package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
  29. package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
  30. package/src/__tests__/host-proxy-base.test.ts +1 -0
  31. package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
  32. package/src/__tests__/llm-schema.test.ts +32 -0
  33. package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
  34. package/src/__tests__/memory-config-file-parity.test.ts +18 -0
  35. package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
  36. package/src/__tests__/messaging-send-tool.test.ts +159 -13
  37. package/src/__tests__/messaging-shared.test.ts +19 -0
  38. package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
  39. package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
  40. package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
  41. package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
  42. package/src/__tests__/schedule-routes.test.ts +30 -1
  43. package/src/__tests__/scheduler-result-notification.test.ts +209 -0
  44. package/src/__tests__/skills-install-extract.test.ts +26 -0
  45. package/src/__tests__/skills-install-staging.test.ts +11 -2
  46. package/src/api/constants/personality-sliders.test.ts +21 -0
  47. package/src/api/constants/personality-sliders.ts +64 -0
  48. package/src/api/index.ts +8 -0
  49. package/src/archive/ustar-size.test.ts +32 -0
  50. package/src/archive/ustar-size.ts +23 -0
  51. package/src/background-wake/next-wake.test.ts +13 -0
  52. package/src/background-wake/next-wake.ts +4 -2
  53. package/src/calls/voice-session-bridge.ts +26 -0
  54. package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
  55. package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
  56. package/src/cli/commands/config.help.ts +3 -1
  57. package/src/cli/commands/memory/worker.ts +1 -1
  58. package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
  59. package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
  60. package/src/cli/commands/platform/credits-format.ts +57 -0
  61. package/src/cli/commands/platform/index.help.ts +19 -3
  62. package/src/cli/commands/plugins.help.ts +4 -1
  63. package/src/cli/commands/plugins.ts +2 -1
  64. package/src/cli/lib/bundled-marketplace.json +1 -1
  65. package/src/cli/lib/install-from-platform.ts +8 -10
  66. package/src/config/bundled-skills/messaging/SKILL.md +2 -0
  67. package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
  68. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
  69. package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
  70. package/src/config/bundled-skills/schedule/SKILL.md +9 -1
  71. package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
  72. package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
  73. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
  74. package/src/config/feature-flag-registry.json +17 -1
  75. package/src/config/input-modalities.test.ts +71 -0
  76. package/src/config/input-modalities.ts +95 -0
  77. package/src/config/schemas/heartbeat.ts +3 -3
  78. package/src/config/schemas/llm.ts +13 -3
  79. package/src/config/schemas/memory-storage.test.ts +39 -0
  80. package/src/config/schemas/memory-storage.ts +21 -0
  81. package/src/context/token-estimator.ts +2 -2
  82. package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
  83. package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
  84. package/src/daemon/conversation-error.ts +20 -7
  85. package/src/daemon/embedding-reconcile.ts +64 -1
  86. package/src/daemon/handlers/config-embeddings.ts +31 -0
  87. package/src/daemon/host-app-control-proxy.ts +2 -0
  88. package/src/daemon/host-bash-proxy.ts +1 -0
  89. package/src/daemon/host-cu-proxy.ts +6 -6
  90. package/src/daemon/host-file-proxy.ts +6 -6
  91. package/src/daemon/host-proxy-base.ts +10 -9
  92. package/src/daemon/host-transfer-proxy.ts +21 -24
  93. package/src/daemon/mcp-reload-service.ts +6 -0
  94. package/src/daemon/providers-setup.ts +5 -26
  95. package/src/heartbeat/heartbeat-service.ts +72 -13
  96. package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
  97. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
  98. package/src/live-voice/live-voice-photo.ts +68 -4
  99. package/src/live-voice/live-voice-session.ts +36 -2
  100. package/src/live-voice/protocol.ts +98 -1
  101. package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
  102. package/src/mcp/__tests__/reload-signal.test.ts +57 -0
  103. package/src/mcp/__tests__/startup.test.ts +150 -0
  104. package/src/mcp/reload-signal.ts +50 -0
  105. package/src/mcp/startup.ts +101 -0
  106. package/src/messaging/providers/outlook/adapter.ts +2 -13
  107. package/src/messaging/providers/outlook/client.ts +13 -1
  108. package/src/messaging/providers/outlook/types.ts +3 -0
  109. package/src/notifications/__tests__/decision-engine.test.ts +79 -6
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
  111. package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
  112. package/src/notifications/copy-composer.ts +33 -11
  113. package/src/notifications/decision-engine.ts +48 -11
  114. package/src/notifications/events-store.ts +68 -2
  115. package/src/notifications/home-feed-side-effect.ts +15 -3
  116. package/src/notifications/schedule-result-producer.ts +275 -0
  117. package/src/notifications/signal.ts +5 -0
  118. package/src/oauth/seed-providers.ts +3 -2
  119. package/src/persistence/conversation-crud.ts +38 -0
  120. package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
  121. package/src/persistence/embeddings/embedding-backend.ts +148 -11
  122. package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
  123. package/src/persistence/embeddings/embedding-openai.ts +55 -4
  124. package/src/persistence/embeddings/embedding-types.test.ts +24 -0
  125. package/src/persistence/embeddings/embedding-types.ts +25 -1
  126. package/src/persistence/job-utils.ts +6 -2
  127. package/src/plugin-api/index.ts +2 -1
  128. package/src/plugin-api/vision-support.test.ts +81 -0
  129. package/src/plugin-api/vision-support.ts +57 -10
  130. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
  131. package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
  132. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
  133. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
  134. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
  135. package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
  136. package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
  137. package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
  138. package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
  139. package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
  140. package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
  141. package/src/providers/anthropic/client.ts +9 -4
  142. package/src/providers/inline-audio-support.test.ts +34 -0
  143. package/src/providers/inline-audio-support.ts +27 -0
  144. package/src/providers/model-catalog.ts +22 -2
  145. package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
  146. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
  147. package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
  148. package/src/providers/openai/api-error-normalization.ts +18 -3
  149. package/src/providers/openai/chat-completions-provider.ts +113 -13
  150. package/src/providers/openai/responses-provider.ts +10 -8
  151. package/src/providers/provider-secret-catalog.ts +11 -1
  152. package/src/providers/vellum/client.ts +14 -1
  153. package/src/providers/vellum/personality-directions.test.ts +163 -0
  154. package/src/providers/vellum/personality-directions.ts +120 -0
  155. package/src/providers/xml-tool-call-salvage.ts +212 -0
  156. package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
  157. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
  158. package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
  159. package/src/runtime/auth/same-actor.ts +29 -5
  160. package/src/runtime/background-job-runner.ts +38 -29
  161. package/src/runtime/guardian-action-service.ts +20 -1
  162. package/src/runtime/migrations/vbundle-validator.ts +3 -3
  163. package/src/runtime/pending-interactions.ts +4 -5
  164. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
  165. package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
  166. package/src/runtime/routes/conversation-query-routes.ts +53 -11
  167. package/src/runtime/routes/document-comments-routes.ts +8 -2
  168. package/src/runtime/routes/guardian-action-routes.ts +7 -0
  169. package/src/runtime/routes/heartbeat-routes.ts +41 -53
  170. package/src/runtime/routes/host-app-control-routes.ts +10 -36
  171. package/src/runtime/routes/host-bash-routes.ts +11 -45
  172. package/src/runtime/routes/host-browser-routes.ts +24 -0
  173. package/src/runtime/routes/host-cu-routes.ts +11 -45
  174. package/src/runtime/routes/host-file-routes.ts +11 -44
  175. package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
  176. package/src/runtime/routes/host-transfer-routes.ts +33 -25
  177. package/src/runtime/routes/index.ts +0 -2
  178. package/src/runtime/routes/integrations/a2a.ts +12 -3
  179. package/src/runtime/routes/integrations/vercel.ts +13 -3
  180. package/src/runtime/routes/platform-routes.ts +121 -24
  181. package/src/runtime/routes/schedule-routes.ts +15 -1
  182. package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
  183. package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
  184. package/src/schedule/schedule-timezone.ts +25 -0
  185. package/src/schedule/scheduler.ts +19 -0
  186. package/src/schedule/worker.ts +119 -2
  187. package/src/skills/catalog-install.ts +40 -5
  188. package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
  189. package/src/tools/browser/browser-execution.ts +117 -53
  190. package/src/tools/browser/browser-status-constants.ts +6 -6
  191. package/src/tools/capability-offer.test.ts +87 -0
  192. package/src/tools/capability-offer.ts +93 -0
  193. package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
  194. package/src/tools/terminal/safe-env.ts +3 -2
  195. package/src/util/provider-error-patterns.ts +34 -0
  196. package/src/watcher/__tests__/engine.test.ts +5 -2
  197. package/src/watcher/engine.ts +7 -4
  198. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
  199. package/src/runtime/routes/sight-frame-routes.ts +0 -136
@@ -35,6 +35,7 @@ mock.module("../runtime/pending-interactions.js", () => ({
35
35
 
36
36
  // Per-test controls for the proxy stub
37
37
  let stubTargetClientId: string | null = null;
38
+ let stubTargetActorPrincipalId: string | undefined;
38
39
  const getTransferContentCalls: string[] = [];
39
40
  const receiveTransferContentCalls: string[] = [];
40
41
  const resolveTransferResultCalls: string[] = [];
@@ -50,6 +51,9 @@ mock.module("../daemon/host-transfer-proxy.js", () => ({
50
51
  return stubTargetClientId;
51
52
  },
52
53
  getTargetActorPrincipalIdForTransfer(_transferId: string) {
54
+ if (stubTargetActorPrincipalId !== undefined) {
55
+ return stubTargetActorPrincipalId;
56
+ }
53
57
  return stubTargetClientId
54
58
  ? clientActors.get(stubTargetClientId)
55
59
  : undefined;
@@ -139,6 +143,7 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
139
143
  beforeEach(() => {
140
144
  pendingStore.clear();
141
145
  stubTargetClientId = null;
146
+ stubTargetActorPrincipalId = undefined;
142
147
  getTransferContentCalls.length = 0;
143
148
  clientActors.clear();
144
149
  });
@@ -236,6 +241,30 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
236
241
  expect(result).toBeInstanceOf(Uint8Array);
237
242
  expect(getTransferContentCalls).toContain(TEST_TRANSFER_ID);
238
243
  });
244
+
245
+ test("rejects a different actor when the transfer recorded a source actor", () => {
246
+ stubTargetClientId = null;
247
+ stubTargetActorPrincipalId = "principal-owner";
248
+ expect(() =>
249
+ handleTransferContentGet({
250
+ pathParams: { transferId: TEST_TRANSFER_ID },
251
+ headers: { "x-vellum-actor-principal-id": "principal-attacker" },
252
+ }),
253
+ ).toThrow(ForbiddenError);
254
+ expect(getTransferContentCalls).toHaveLength(0);
255
+ });
256
+
257
+ test("accepts the recorded source actor on an untargeted transfer", async () => {
258
+ stubTargetClientId = null;
259
+ stubTargetActorPrincipalId = "principal-owner";
260
+ const result = await handleTransferContentGet({
261
+ pathParams: { transferId: TEST_TRANSFER_ID },
262
+ headers: { "x-vellum-actor-principal-id": "principal-owner" },
263
+ });
264
+
265
+ expect(result).toBeInstanceOf(Uint8Array);
266
+ expect(getTransferContentCalls).toContain(TEST_TRANSFER_ID);
267
+ });
239
268
  });
240
269
 
241
270
  // ── 5. Same-user actor binding (defense-in-depth) ─────────────────────────
@@ -289,6 +318,7 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
289
318
  beforeEach(() => {
290
319
  pendingStore.clear();
291
320
  stubTargetClientId = null;
321
+ stubTargetActorPrincipalId = undefined;
292
322
  receiveTransferContentCalls.length = 0;
293
323
  clientActors.clear();
294
324
  });
@@ -408,6 +438,38 @@ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
408
438
  expect(result).toEqual({ accepted: true });
409
439
  expect(receiveTransferContentCalls).toContain(TEST_TRANSFER_ID);
410
440
  });
441
+
442
+ test("rejects a different actor when the transfer recorded a source actor", async () => {
443
+ stubTargetClientId = null;
444
+ stubTargetActorPrincipalId = "principal-owner";
445
+ await expect(
446
+ handleTransferContentPut({
447
+ pathParams: { transferId: TEST_TRANSFER_ID },
448
+ headers: {
449
+ "x-vellum-actor-principal-id": "principal-attacker",
450
+ "x-transfer-sha256": "abc",
451
+ },
452
+ rawBody: new Uint8Array(Buffer.from("data")),
453
+ }),
454
+ ).rejects.toBeInstanceOf(ForbiddenError);
455
+ expect(receiveTransferContentCalls).toHaveLength(0);
456
+ });
457
+
458
+ test("accepts the recorded source actor on an untargeted transfer", async () => {
459
+ stubTargetClientId = null;
460
+ stubTargetActorPrincipalId = "principal-owner";
461
+ const result = await handleTransferContentPut({
462
+ pathParams: { transferId: TEST_TRANSFER_ID },
463
+ headers: {
464
+ "x-vellum-actor-principal-id": "principal-owner",
465
+ "x-transfer-sha256": "abc",
466
+ },
467
+ rawBody: new Uint8Array(Buffer.from("data")),
468
+ });
469
+
470
+ expect(result).toEqual({ accepted: true });
471
+ expect(receiveTransferContentCalls).toContain(TEST_TRANSFER_ID);
472
+ });
411
473
  });
412
474
 
413
475
  // ── 5. Same-user actor binding (defense-in-depth) ─────────────────────────
@@ -469,6 +531,7 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
469
531
  beforeEach(() => {
470
532
  pendingStore.clear();
471
533
  stubTargetClientId = null;
534
+ stubTargetActorPrincipalId = undefined;
472
535
  resolveTransferResultCalls.length = 0;
473
536
  clientActors.clear();
474
537
  });
@@ -599,6 +662,29 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
599
662
 
600
663
  expect(result).toEqual({ accepted: true });
601
664
  });
665
+
666
+ test("rejects a different actor when the pending request recorded a source actor", () => {
667
+ registerPending({ targetActorPrincipalId: "principal-owner" });
668
+ expect(() =>
669
+ handleTransferResult({
670
+ body: resultBody(),
671
+ headers: { "x-vellum-actor-principal-id": "principal-attacker" },
672
+ }),
673
+ ).toThrow(ForbiddenError);
674
+ expect(resolveTransferResultCalls).toHaveLength(0);
675
+ expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
676
+ });
677
+
678
+ test("accepts the recorded source actor on an untargeted pending request", async () => {
679
+ registerPending({ targetActorPrincipalId: "principal-owner" });
680
+ const result = await handleTransferResult({
681
+ body: resultBody(),
682
+ headers: { "x-vellum-actor-principal-id": "principal-owner" },
683
+ });
684
+
685
+ expect(result).toEqual({ accepted: true });
686
+ expect(resolveTransferResultCalls).toContain(TEST_REQUEST_ID);
687
+ });
602
688
  });
603
689
 
604
690
  // ── 5. Same-user actor binding (defense-in-depth) ─────────────────────────
@@ -358,4 +358,36 @@ describe("LLMSchema", () => {
358
358
  });
359
359
  expect(result.success).toBe(true);
360
360
  });
361
+
362
+ test("a mix profile with inputModalities is rejected", () => {
363
+ const result = LLMSchema.safeParse({
364
+ profiles: {
365
+ combo: {
366
+ mix: [
367
+ { profile: "fast", weight: 1 },
368
+ { profile: "slow", weight: 1 },
369
+ ],
370
+ inputModalities: { image: { enabled: true, supported: true } },
371
+ },
372
+ fast: { provider: "anthropic", model: "claude-haiku-4-5" },
373
+ slow: { provider: "anthropic", model: "claude-opus-4-6" },
374
+ },
375
+ });
376
+ expect(result.success).toBe(false);
377
+ });
378
+
379
+ test("a custom profile may persist inputModalities", () => {
380
+ const parsed = LLMSchema.parse({
381
+ profiles: {
382
+ gateway: {
383
+ provider: "openai-compatible",
384
+ model: "qwen2.5-vl",
385
+ inputModalities: { image: { enabled: true, supported: true } },
386
+ },
387
+ },
388
+ });
389
+ expect(parsed.profiles.gateway?.inputModalities).toEqual({
390
+ image: { enabled: true, supported: true },
391
+ });
392
+ });
361
393
  });
@@ -82,6 +82,8 @@ mock.module("../persistence/embeddings/embedding-backend.js", () => ({
82
82
  vectors: [],
83
83
  }),
84
84
  geminiCacheExtras: () => [],
85
+ customCacheExtras: () => [],
86
+ durableEmbeddingCacheExtras: () => [],
85
87
  generateSparseEmbedding: () => ({ indices: [], values: [] }),
86
88
  getMemoryBackendStatus: async () => ({
87
89
  enabled: false,
@@ -101,6 +101,24 @@ describe("memory config file parity", () => {
101
101
  expect(memory.embeddings.provider).toBe("openai");
102
102
  });
103
103
 
104
+ test("custom embedding endpoint knobs are visible and identical", () => {
105
+ writeConfig({
106
+ memory: {
107
+ embeddings: {
108
+ provider: "custom",
109
+ baseUrl: "http://127.0.0.1:4000/v1",
110
+ customModel: "text-embedding-3-small",
111
+ customDimensions: 1024,
112
+ },
113
+ },
114
+ });
115
+ const memory = expectParity();
116
+ expect(memory.embeddings.provider).toBe("custom");
117
+ expect(memory.embeddings.baseUrl).toBe("http://127.0.0.1:4000/v1");
118
+ expect(memory.embeddings.customModel).toBe("text-embedding-3-small");
119
+ expect(memory.embeddings.customDimensions).toBe(1024);
120
+ });
121
+
104
122
  test("cross-field invariant: segmentation overlap >= target falls back like the loader", () => {
105
123
  writeConfig({
106
124
  memory: {
@@ -202,6 +202,21 @@ beforeEach(() => {
202
202
  });
203
203
 
204
204
  describe("user-prompt-submit hook (memory retrieval)", () => {
205
+ test("does not run on a memory-retrospective wake", async () => {
206
+ const { memory, prepareMemoryMock, recordPkbQueryVectorsMock } =
207
+ makeFakeGraphMemory();
208
+ const conversation = installConversation(memory, { trusted: true });
209
+ conversation.currentCallSite = "memoryRetrospective";
210
+ const ctx = makeHookCtx({ conversationId: "conv-retro" });
211
+
212
+ await userPromptSubmitMemoryRetrieval(ctx);
213
+
214
+ expect(prepareMemoryMock).not.toHaveBeenCalled();
215
+ expect(recordPkbQueryVectorsMock).not.toHaveBeenCalled();
216
+ expect(applyRuntimeInjectionsMock).not.toHaveBeenCalled();
217
+ expect(updateMessageMetadataMock).not.toHaveBeenCalled();
218
+ });
219
+
205
220
  test("voice front door skips serial legacy retrieval", async () => {
206
221
  const { memory, prepareMemoryMock, recordPkbQueryVectorsMock } =
207
222
  makeFakeGraphMemory();
@@ -44,14 +44,17 @@ const telegramProvider = makeProvider("telegram", "Telegram");
44
44
  const outlookProvider = makeProvider("outlook", "Outlook");
45
45
  let provider: MessagingProvider = phoneProvider;
46
46
 
47
+ let connection: OAuthConnection | undefined = undefined;
48
+
47
49
  mock.module("../config/bundled-skills/messaging/tools/shared.js", () => ({
48
50
  resolveProvider: () => provider,
49
- getProviderConnection: () => undefined,
51
+ getProviderConnection: () => connection,
50
52
  ok: (content: string) => ({ content, isError: false }),
51
53
  err: (content: string) => ({ content, isError: true }),
52
54
  extractHeader: () => "",
53
55
  parseAddressList: () => [],
54
56
  extractEmail: (a: string) => a.toLowerCase(),
57
+ isMailboxAddress: (value: string) => value.includes("@"),
55
58
  }));
56
59
 
57
60
  // ── Sent-post record dependency mocks ──
@@ -75,6 +78,38 @@ const recordDeliveredChannelPostMock = mock(
75
78
  async (_post: Record<string, unknown>) => ({ messageId: "row-1" }),
76
79
  );
77
80
 
81
+ const mockOutlookCreateDraft = mock(
82
+ async (_conn: OAuthConnection, _draft: Record<string, unknown>) => ({
83
+ id: "outlook-draft-1",
84
+ conversationId: "conv-draft",
85
+ subject: "Docs",
86
+ webLink: "https://outlook.office.com/mail/drafts/id/outlook-draft-1",
87
+ }),
88
+ );
89
+
90
+ const mockOutlookCreateReplyDraft = mock(
91
+ async (_conn: OAuthConnection, _messageId: string, _comment?: string) => ({
92
+ id: "outlook-reply-draft-1",
93
+ conversationId: "conv-reply",
94
+ subject: "Re: Hello",
95
+ webLink: "https://outlook.office.com/mail/drafts/id/outlook-reply-draft-1",
96
+ }),
97
+ );
98
+
99
+ mock.module("../messaging/providers/outlook/client.js", () => ({
100
+ createDraft: mockOutlookCreateDraft,
101
+ createReplyDraft: mockOutlookCreateReplyDraft,
102
+ toOutlookFileAttachments: (
103
+ attachments: Array<{ filename: string; mimeType: string; data: Buffer }>,
104
+ ) =>
105
+ attachments.map((att) => ({
106
+ "@odata.type": "#microsoft.graph.fileAttachment",
107
+ name: att.filename,
108
+ contentType: att.mimeType,
109
+ contentBytes: att.data.toString("base64"),
110
+ })),
111
+ }));
112
+
78
113
  mock.module("../persistence/conversation-crud.js", () => ({
79
114
  setConversationProcessingStartedAt: () => {},
80
115
  isConversationProcessing: () => false,
@@ -99,7 +134,10 @@ import { run } from "../config/bundled-skills/messaging/tools/messaging-send.js"
99
134
  describe("messaging-send tool", () => {
100
135
  beforeEach(() => {
101
136
  provider = phoneProvider;
137
+ connection = undefined;
102
138
  sendMessageMock.mockClear();
139
+ mockOutlookCreateDraft.mockClear();
140
+ mockOutlookCreateReplyDraft.mockClear();
103
141
  getConversationMock.mockClear();
104
142
  syncMessageToDiskMock.mockClear();
105
143
  resolveProactiveHomeConversationMock.mockClear();
@@ -195,11 +233,15 @@ describe("messaging-send tool", () => {
195
233
  expect(sendMessageMock).not.toHaveBeenCalled();
196
234
  });
197
235
 
198
- test("reads and forwards attachments to attachment-capable non-Gmail providers", async () => {
236
+ test("reads and forwards attachments onto an Outlook draft", async () => {
199
237
  const dir = mkdtempSync(join(tmpdir(), "msg-send-att-"));
200
238
  const filePath = join(dir, "report.pdf");
201
239
  writeFileSync(filePath, "pdf-bytes");
202
240
  provider = outlookProvider;
241
+ connection = {
242
+ id: "outlook-conn-1",
243
+ provider: "outlook",
244
+ } as OAuthConnection;
203
245
 
204
246
  try {
205
247
  const result = await run(
@@ -219,17 +261,21 @@ describe("messaging-send tool", () => {
219
261
  );
220
262
 
221
263
  expect(result.isError).toBe(false);
222
- const options = sendMessageMock.mock.calls[0][3] as {
223
- attachments?: Array<{
224
- filename: string;
225
- mimeType: string;
226
- data: Buffer;
227
- }>;
228
- };
229
- expect(options.attachments).toHaveLength(1);
230
- expect(options.attachments?.[0]?.filename).toBe("report.pdf");
231
- expect(options.attachments?.[0]?.mimeType).toBe("application/pdf");
232
- expect(options.attachments?.[0]?.data.toString()).toBe("pdf-bytes");
264
+ expect(result.content).toContain("Outlook draft created");
265
+ expect(result.content).toContain("Draft ID: outlook-draft-1");
266
+ expect(sendMessageMock).not.toHaveBeenCalled();
267
+ expect(mockOutlookCreateDraft).toHaveBeenCalledTimes(1);
268
+ expect(mockOutlookCreateDraft).toHaveBeenCalledWith(
269
+ connection,
270
+ expect.objectContaining({
271
+ subject: "Docs",
272
+ body: { contentType: "text", content: "see attached" },
273
+ toRecipients: [
274
+ { emailAddress: { address: "user@example.com" } },
275
+ ],
276
+ attachments: [expect.objectContaining({ name: "report.pdf" })],
277
+ }),
278
+ );
233
279
  } finally {
234
280
  rmSync(dir, { recursive: true, force: true });
235
281
  }
@@ -305,6 +351,10 @@ describe("messaging-send tool", () => {
305
351
 
306
352
  test("records nothing for a provider that is not a channel", async () => {
307
353
  provider = outlookProvider;
354
+ connection = {
355
+ id: "outlook-conn-1",
356
+ provider: "outlook",
357
+ } as OAuthConnection;
308
358
 
309
359
  await run(
310
360
  {
@@ -324,6 +374,102 @@ describe("messaging-send tool", () => {
324
374
  expect(recordDeliveredChannelPostMock).not.toHaveBeenCalled();
325
375
  });
326
376
 
377
+ test("creates an Outlook draft without a To address when conversation_id is not an email", async () => {
378
+ provider = outlookProvider;
379
+ connection = {
380
+ id: "outlook-conn-1",
381
+ provider: "outlook",
382
+ } as OAuthConnection;
383
+
384
+ const result = await run(
385
+ {
386
+ platform: "outlook",
387
+ conversation_id: "drafts",
388
+ text: "Hey team, here is the update.",
389
+ subject: "Team update",
390
+ },
391
+ {
392
+ workingDir: "/tmp",
393
+ conversationId: "conv-1",
394
+ assistantId: "ast-alpha",
395
+ trustClass: "guardian" as const,
396
+ },
397
+ );
398
+
399
+ expect(result.isError).toBe(false);
400
+ expect(result.content).toContain("Outlook draft created");
401
+ expect(result.content).toContain("No recipient set");
402
+ expect(result.content).toContain("Open it:");
403
+ expect(sendMessageMock).not.toHaveBeenCalled();
404
+ expect(mockOutlookCreateDraft).toHaveBeenCalledWith(
405
+ connection,
406
+ expect.objectContaining({
407
+ subject: "Team update",
408
+ body: {
409
+ contentType: "text",
410
+ content: "Hey team, here is the update.",
411
+ },
412
+ }),
413
+ );
414
+ const draftArg = mockOutlookCreateDraft.mock.calls[0][1];
415
+ expect(draftArg.toRecipients).toBeUndefined();
416
+ });
417
+
418
+ test("creates an Outlook reply draft instead of sending when in_reply_to is set", async () => {
419
+ provider = outlookProvider;
420
+ connection = {
421
+ id: "outlook-conn-1",
422
+ provider: "outlook",
423
+ } as OAuthConnection;
424
+
425
+ const result = await run(
426
+ {
427
+ platform: "outlook",
428
+ conversation_id: "user@example.com",
429
+ text: "Thanks, that works.",
430
+ in_reply_to: "AAMk-original",
431
+ },
432
+ {
433
+ workingDir: "/tmp",
434
+ conversationId: "conv-1",
435
+ assistantId: "ast-alpha",
436
+ trustClass: "guardian" as const,
437
+ },
438
+ );
439
+
440
+ expect(result.isError).toBe(false);
441
+ expect(mockOutlookCreateReplyDraft).toHaveBeenCalledWith(
442
+ connection,
443
+ "AAMk-original",
444
+ "Thanks, that works.",
445
+ );
446
+ expect(sendMessageMock).not.toHaveBeenCalled();
447
+ expect(result.content).toContain("outlook-reply-draft-1");
448
+ });
449
+
450
+ test("errors when Outlook is not connected", async () => {
451
+ provider = outlookProvider;
452
+ connection = undefined;
453
+
454
+ const result = await run(
455
+ {
456
+ platform: "outlook",
457
+ conversation_id: "user@example.com",
458
+ text: "hello",
459
+ },
460
+ {
461
+ workingDir: "/tmp",
462
+ conversationId: "conv-1",
463
+ assistantId: "ast-alpha",
464
+ trustClass: "guardian" as const,
465
+ },
466
+ );
467
+
468
+ expect(result.isError).toBe(true);
469
+ expect(result.content).toContain("OAuth connection");
470
+ expect(mockOutlookCreateDraft).not.toHaveBeenCalled();
471
+ });
472
+
327
473
  test("a record failure does not fail the send", async () => {
328
474
  provider = telegramProvider;
329
475
  getConversationMock.mockImplementation(() => ({
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { isMailboxAddress } from "../config/bundled-skills/messaging/tools/shared.js";
4
+
5
+ describe("isMailboxAddress", () => {
6
+ test("accepts a bare email", () => {
7
+ expect(isMailboxAddress("user@example.com")).toBe(true);
8
+ });
9
+
10
+ test("accepts a display-name address", () => {
11
+ expect(isMailboxAddress("Alice <alice@example.org>")).toBe(true);
12
+ });
13
+
14
+ test("rejects placeholders used when the recipient is unknown", () => {
15
+ expect(isMailboxAddress("drafts")).toBe(false);
16
+ expect(isMailboxAddress("outlook")).toBe(false);
17
+ expect(isMailboxAddress("")).toBe(false);
18
+ });
19
+ });