@vellumai/assistant 0.9.0 → 0.9.1-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 (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -1,16 +1,14 @@
1
1
  /**
2
- * Tests for the address-fallback path in resolveActorTrust.
2
+ * Tests for address-based member resolution in resolveActorTrust.
3
3
  *
4
- * Phone channels registered by the inbound name-capture flow have `address`
5
- * set (E.164) but `externalUserId` = NULL until DTMF verification completes.
6
- * The primary `findContactByChannelExternalId` lookup misses these channels,
7
- * which historically caused the relay router to fall into the name-capture
8
- * ("I don't recognize this number") path instead of the unverified-caller
9
- * guidance path.
4
+ * All member lookups use the canonical (type, address) path — the address
5
+ * column is the sole identity for all channel types. Phone channels
6
+ * registered by the inbound name-capture flow have `address` set (E.164)
7
+ * and are discovered through the same path as every other channel.
10
8
  *
11
- * This suite verifies that the address-based fallback fires correctly and
12
- * that the returned `memberRecord` carries the right channel/status so
13
- * relay-setup-router can emit the `unverified_caller` outcome.
9
+ * This suite verifies that address-based lookup returns the correct
10
+ * `memberRecord` with the right channel/status so relay-setup-router
11
+ * can emit the appropriate outcome (e.g. `unverified_caller`).
14
12
  */
15
13
 
16
14
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -22,18 +20,14 @@ mock.module("../util/logger.js", () => ({
22
20
  }));
23
21
 
24
22
  // ── Contact store stubs — filled in per test ─────────────────────────────────
25
- let _byExternalId: ReturnType<
26
- typeof import("../contacts/contact-store.js")["findContactByChannelExternalId"]
27
- > = null;
28
23
  let _byAddress: ReturnType<
29
- typeof import("../contacts/contact-store.js")["findContactByAddress"]
24
+ (typeof import("../contacts/contact-store.js"))["findContactByAddress"]
30
25
  > = null;
31
26
  let _guardian: ReturnType<
32
- typeof import("../contacts/contact-store.js")["findGuardianForChannel"]
27
+ (typeof import("../contacts/contact-store.js"))["findGuardianForChannel"]
33
28
  > = null;
34
29
 
35
30
  mock.module("../contacts/contact-store.js", () => ({
36
- findContactByChannelExternalId: (_type: string, _id: string) => _byExternalId,
37
31
  findContactByAddress: (_type: string, _addr: string) => _byAddress,
38
32
  findGuardianForChannel: (_channel: string) => _guardian,
39
33
  }));
@@ -69,7 +63,7 @@ function makeContact(
69
63
  id: channelId,
70
64
  contactId: "contact-test",
71
65
  type: "phone",
72
- address: PHONE.toLowerCase(),
66
+ address: PHONE,
73
67
  externalUserId,
74
68
  externalChatId: null,
75
69
  isPrimary: true,
@@ -94,7 +88,6 @@ function makeContact(
94
88
 
95
89
  describe("resolveActorTrust — address fallback", () => {
96
90
  beforeEach(() => {
97
- _byExternalId = null;
98
91
  _byAddress = null;
99
92
  _guardian = null;
100
93
  });
@@ -117,12 +110,8 @@ describe("resolveActorTrust — address fallback", () => {
117
110
  expect(result.trustClass).toBe("unknown");
118
111
  });
119
112
 
120
- test("externalUserId path takes priority over address path", () => {
121
- // Both lookups return a contact; the externalUserId one should win.
122
- const primaryContact = makeContact("contact", "active", PHONE);
123
- const addressContact = makeContact("contact", "unverified", null);
124
- _byExternalId = primaryContact;
125
- _byAddress = addressContact;
113
+ test("address lookup is the sole member resolution path", () => {
114
+ _byAddress = makeContact("contact", "active", null);
126
115
 
127
116
  const result = resolveActorTrust({
128
117
  assistantId: "asst-1",
@@ -132,11 +121,10 @@ describe("resolveActorTrust — address fallback", () => {
132
121
  });
133
122
 
134
123
  expect(result.memberRecord?.channel.status).toBe("active");
135
- expect(result.memberRecord?.channel.externalUserId).toBe(PHONE);
124
+ expect(result.memberRecord?.channel.address).toBe(PHONE);
136
125
  });
137
126
 
138
127
  test("returns null memberRecord when neither lookup finds the number", () => {
139
- _byExternalId = null;
140
128
  _byAddress = null;
141
129
 
142
130
  const result = resolveActorTrust({
@@ -0,0 +1,240 @@
1
+ /**
2
+ * The compaction summarizer's input routing.
3
+ *
4
+ * Budget-gate compaction summarizes the *injected* history so the summary
5
+ * call's prompt prefix matches the agent's warm prefix cache (a cache read
6
+ * rather than a fresh cache write). Overflow recovery summarizes the
7
+ * injection-*stripped* history because its emergency split keeps an
8
+ * un-stripped verbatim prefix that the post-compaction re-injection would
9
+ * otherwise double-stack.
10
+ */
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import type { PostCompactContext } from "@vellumai/plugin-api";
14
+
15
+ import type { AgentEvent } from "../agent/loop.js";
16
+ import { AgentLoop } from "../agent/loop.js";
17
+ import type { ContextWindowConfig } from "../config/types.js";
18
+ import type { TrustContext } from "../daemon/trust-context.js";
19
+ import { HOOKS } from "../plugin-api/constants.js";
20
+ import {
21
+ createContextWindowManager,
22
+ disposeContextWindowManager,
23
+ getContextWindowManager,
24
+ } from "../plugins/defaults/compaction/manager-store.js";
25
+ import {
26
+ registerPlugin,
27
+ resetPluginRegistryForTests,
28
+ } from "../plugins/registry.js";
29
+ import type {
30
+ Message,
31
+ Provider,
32
+ ProviderResponse,
33
+ SendMessageOptions,
34
+ } from "../providers/types.js";
35
+ import { ContextOverflowError } from "../providers/types.js";
36
+
37
+ const testPostCompactPlugin = {
38
+ manifest: { name: "test-post-compact", version: "0.0.0" },
39
+ hooks: {
40
+ [HOOKS.POST_COMPACT]: async (input: PostCompactContext): Promise<void> => {
41
+ void input;
42
+ },
43
+ },
44
+ };
45
+
46
+ const CONVERSATION_ID = "compaction-strip-conversation";
47
+
48
+ /**
49
+ * A runtime `<workspace>` injection block. `stripInjectionsForCompaction`
50
+ * matches its full `{ prefix: "<workspace>\n", suffix: "\n</workspace>" }`
51
+ * wrapper and drops it from user-message content.
52
+ */
53
+ const WORKSPACE_INJECTION =
54
+ "<workspace>\nActive workspace: project-x\n</workspace>";
55
+ const TURN_BODY = "Hello there, this is the turn body.";
56
+
57
+ /**
58
+ * A user turn carrying a real text block plus a runtime injection block.
59
+ * Stripping drops the injection block and keeps the real one.
60
+ */
61
+ const injectedUserMessage: Message = {
62
+ role: "user",
63
+ content: [
64
+ { type: "text", text: TURN_BODY },
65
+ { type: "text", text: WORKSPACE_INJECTION },
66
+ ],
67
+ };
68
+
69
+ function textResponse(text: string): ProviderResponse {
70
+ return {
71
+ content: [{ type: "text", text }],
72
+ model: "mock-model",
73
+ usage: { inputTokens: 10, outputTokens: 5 },
74
+ stopReason: "end_turn",
75
+ };
76
+ }
77
+
78
+ function createMockProvider(responses: ProviderResponse[]): Provider {
79
+ let callIndex = 0;
80
+ return {
81
+ name: "mock",
82
+ async sendMessage(
83
+ _messages: Message[],
84
+ _options?: SendMessageOptions,
85
+ ): Promise<ProviderResponse> {
86
+ const response = responses[callIndex] ?? responses[responses.length - 1];
87
+ callIndex++;
88
+ return response;
89
+ },
90
+ };
91
+ }
92
+
93
+ interface CompactionInputCapture {
94
+ budget: Message[] | null;
95
+ overflow: Message[] | null;
96
+ }
97
+
98
+ /**
99
+ * Install a per-conversation manager that records the messages handed to the
100
+ * budget summarizer (`maybeCompact`) and to overflow recovery
101
+ * (`recoverContextOverflow`).
102
+ */
103
+ function installCapturingManager(capture: CompactionInputCapture): {
104
+ trust: TrustContext;
105
+ } {
106
+ createContextWindowManager({
107
+ provider: { name: "mock-provider" } as unknown as Provider,
108
+ systemPrompt: "system",
109
+ config: {} as unknown as ContextWindowConfig,
110
+ conversationId: CONVERSATION_ID,
111
+ });
112
+ const manager = getContextWindowManager(CONVERSATION_ID);
113
+ if (manager) {
114
+ manager.maybeCompact = (async (messages: Message[]) => {
115
+ capture.budget = messages;
116
+ return {
117
+ messages: [injectedUserMessage],
118
+ compacted: true,
119
+ exhausted: false,
120
+ };
121
+ }) as unknown as typeof manager.maybeCompact;
122
+ manager.recoverContextOverflow = (async (messages: Message[]) => {
123
+ capture.overflow = messages;
124
+ return {
125
+ messages: [injectedUserMessage],
126
+ compacted: true,
127
+ exhausted: false,
128
+ };
129
+ }) as unknown as typeof manager.recoverContextOverflow;
130
+ }
131
+ return { trust: { sourceChannel: "vellum", trustClass: "unknown" } };
132
+ }
133
+
134
+ function serialize(messages: Message[] | null): string {
135
+ return JSON.stringify(messages ?? []);
136
+ }
137
+
138
+ describe("AgentLoop compaction summarizer input", () => {
139
+ beforeEach(() => {
140
+ resetPluginRegistryForTests();
141
+ registerPlugin(testPostCompactPlugin);
142
+ });
143
+
144
+ afterEach(() => {
145
+ disposeContextWindowManager(CONVERSATION_ID);
146
+ });
147
+
148
+ test("budget-gate compaction summarizes the injected history", async () => {
149
+ // GIVEN a history whose user turn carries a runtime-injection block
150
+ const capture: CompactionInputCapture = { budget: null, overflow: null };
151
+ const provider = createMockProvider([
152
+ textResponse("done after compaction"),
153
+ ]);
154
+ const loop = new AgentLoop({
155
+ provider,
156
+ systemPrompt: "system",
157
+ conversationId: CONVERSATION_ID,
158
+ tools: [],
159
+ toolExecutor: async () => ({ content: "ok", isError: false }),
160
+ });
161
+ const events: AgentEvent[] = [];
162
+
163
+ // WHEN the budget gate trips and in-place compaction runs
164
+ await loop.run({
165
+ requestId: "req",
166
+ messages: [injectedUserMessage],
167
+ onEvent: (event) => {
168
+ events.push(event);
169
+ },
170
+ resolveContextWindow: () => ({
171
+ maxInputTokens: 10,
172
+ overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
173
+ }),
174
+ compactInPlace: true,
175
+ ...installCapturingManager(capture),
176
+ });
177
+
178
+ // THEN the budget summarizer received the injected history, injection intact
179
+ expect(capture.budget).not.toBeNull();
180
+ expect(serialize(capture.budget)).toContain("<workspace>");
181
+ // AND overflow recovery was not invoked
182
+ expect(capture.overflow).toBeNull();
183
+ // AND the history-stripped marker still fires for the durable base
184
+ expect(events.some((e) => e.type === "history_stripped")).toBe(true);
185
+ });
186
+
187
+ test("overflow-driven compaction summarizes the injection-stripped history", async () => {
188
+ // GIVEN a history whose user turn carries a runtime-injection block
189
+ const capture: CompactionInputCapture = { budget: null, overflow: null };
190
+
191
+ // AND a provider that rejects the first call as context-too-large
192
+ let throwOnce = true;
193
+ const provider: Provider = {
194
+ name: "mock",
195
+ async sendMessage(): Promise<ProviderResponse> {
196
+ if (throwOnce) {
197
+ throwOnce = false;
198
+ throw new ContextOverflowError("prompt too long", "mock", {
199
+ actualTokens: 999_999,
200
+ });
201
+ }
202
+ return textResponse("done after overflow recovery");
203
+ },
204
+ };
205
+ const loop = new AgentLoop({
206
+ provider,
207
+ systemPrompt: "system",
208
+ conversationId: CONVERSATION_ID,
209
+ tools: [],
210
+ toolExecutor: async () => ({ content: "ok", isError: false }),
211
+ });
212
+ // A blocking watermark plus `compactInPlace: false` keep the budget gate
213
+ // from firing, so only the overflow rejection drives compaction.
214
+ loop.compactionCircuit.lastPostCompactionEstimate = 0;
215
+ const events: AgentEvent[] = [];
216
+
217
+ // WHEN the provider overflow forces overflow recovery
218
+ await loop.run({
219
+ requestId: "req",
220
+ messages: [injectedUserMessage],
221
+ onEvent: (event) => {
222
+ events.push(event);
223
+ },
224
+ resolveContextWindow: () => ({
225
+ maxInputTokens: 10,
226
+ overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
227
+ }),
228
+ compactInPlace: false,
229
+ ...installCapturingManager(capture),
230
+ });
231
+
232
+ // THEN overflow recovery received the history with the injection removed
233
+ expect(capture.overflow).not.toBeNull();
234
+ expect(serialize(capture.overflow)).not.toContain("<workspace>");
235
+ // AND the real turn body survives the strip
236
+ expect(serialize(capture.overflow)).toContain(TURN_BODY);
237
+ // AND the budget summarizer was not invoked
238
+ expect(capture.budget).toBeNull();
239
+ });
240
+ });
@@ -365,6 +365,75 @@ describe("agent loop output hooks", () => {
365
365
  expect(calls[0].systemPrompt).toContain("[EDITED]");
366
366
  });
367
367
 
368
+ test("pre-model-call routes the call to the hook's chosen inference profile", async () => {
369
+ // GIVEN a hook that selects an inference profile for the user-facing call
370
+ registerOutputHookPlugin({
371
+ preModelCall: (ctx) => {
372
+ if (ctx.callSite !== "mainAgent") return;
373
+ ctx.modelProfile = "fast-profile";
374
+ },
375
+ });
376
+ const { provider, calls } = createMockProvider([textResponse("hi")]);
377
+ const loop = new AgentLoop({
378
+ provider: provider,
379
+ systemPrompt: "system",
380
+ conversationId: "test-conversation",
381
+ });
382
+
383
+ // WHEN the loop issues the user-facing provider call
384
+ await loop.run({
385
+ requestId: "test-request",
386
+ messages: [userMessage],
387
+ onEvent: collect([]),
388
+ callSite: "mainAgent",
389
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
390
+ });
391
+
392
+ // THEN the provider call carries the hook's profile as the override
393
+ expect(calls[0].options?.config?.overrideProfile).toBe("fast-profile");
394
+ });
395
+
396
+ test("pre-model-call seeds modelProfile from the resolved override and clearing it drops the override", async () => {
397
+ // GIVEN a hook that observes the seeded override and then clears it
398
+ let seeded: string | null | undefined;
399
+ registerOutputHookPlugin({
400
+ preModelCall: (ctx) => {
401
+ seeded = ctx.modelProfile;
402
+ ctx.modelProfile = null;
403
+ },
404
+ });
405
+ const { provider, calls } = createMockProvider([textResponse("hi")]);
406
+ const loop = new AgentLoop({
407
+ provider: provider,
408
+ systemPrompt: "system",
409
+ conversationId: "test-conversation",
410
+ });
411
+
412
+ // WHEN the loop runs with an ad-hoc override profile
413
+ await loop.run({
414
+ requestId: "test-request",
415
+ messages: [userMessage],
416
+ onEvent: collect([]),
417
+ callSite: "mainAgent",
418
+ overrideProfile: "seed-profile",
419
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
420
+ });
421
+
422
+ // THEN the hook saw the loop's resolved override
423
+ expect(seeded).toBe("seed-profile");
424
+ // AND clearing it sends no override on the provider call
425
+ expect(calls[0].options?.config?.overrideProfile).toBeUndefined();
426
+ });
427
+
428
+ // Context-window sizing and overflow recovery read the profile resolved
429
+ // before the hook runs, so a hook routing to a smaller-window profile does
430
+ // not resize the budget gate for that call. Tracked for a follow-up that
431
+ // resolves the routed profile ahead of the context-sizing gate.
432
+ test.todo(
433
+ "pre-model-call routing resizes the context-window budget for the routed profile",
434
+ () => {},
435
+ );
436
+
368
437
  test("fail-open: a hook that mutates in place AND then throws cannot corrupt the persisted content", async () => {
369
438
  // The hook mutates the array it receives before throwing. If the loop
370
439
  // handed the hook the real `assistantMessage.content` array, the
@@ -195,6 +195,7 @@ interface CapturedRunAgentLoopOptions {
195
195
  titleText?: string;
196
196
  callSite?: string;
197
197
  overrideProfile?: string;
198
+ forceOverrideProfile?: boolean;
198
199
  }
199
200
 
200
201
  const capturedRunAgentLoopOptions: CapturedRunAgentLoopOptions[] = [];
@@ -323,6 +324,29 @@ describe("SubagentManager.spawn — overrideProfile inheritance", () => {
323
324
  const captured = capturedRunAgentLoopOptions[0];
324
325
  expect(captured.callSite).toBe("subagentSpawn");
325
326
  expect(captured.overrideProfile).toBe("fast");
327
+ expect("forceOverrideProfile" in captured).toBe(false);
328
+ });
329
+
330
+ test("forwards forced overrideProfile from SubagentConfig into runAgentLoop", async () => {
331
+ capturedRunAgentLoopOptions.length = 0;
332
+
333
+ const manager = new SubagentManager();
334
+ await manager.spawn(
335
+ {
336
+ parentConversationId: "parent-forced",
337
+ label: "child",
338
+ objective: "do the thing",
339
+ overrideProfile: "fast",
340
+ forceOverrideProfile: true,
341
+ },
342
+ () => {},
343
+ );
344
+
345
+ expect(capturedRunAgentLoopOptions).toHaveLength(1);
346
+ const captured = capturedRunAgentLoopOptions[0];
347
+ expect(captured.callSite).toBe("subagentSpawn");
348
+ expect(captured.overrideProfile).toBe("fast");
349
+ expect(captured.forceOverrideProfile).toBe(true);
326
350
  });
327
351
 
328
352
  test("omits overrideProfile when SubagentConfig does not set it", async () => {
@@ -344,6 +368,7 @@ describe("SubagentManager.spawn — overrideProfile inheritance", () => {
344
368
  // Field must be absent rather than carrying `undefined`, mirroring the
345
369
  // agent loop's "field omitted when unset" contract.
346
370
  expect("overrideProfile" in captured).toBe(false);
371
+ expect("forceOverrideProfile" in captured).toBe(false);
347
372
  });
348
373
  });
349
374
 
@@ -28,7 +28,7 @@ afterAll(() => {
28
28
  });
29
29
 
30
30
  describe("always-loaded tool count", () => {
31
- test("should be exactly 11 with recall occupying the existing slot", async () => {
31
+ test("should be exactly 10 with recall occupying the existing slot", async () => {
32
32
  await initializeTools();
33
33
  const allDefs = getAllToolDefinitions();
34
34
 
@@ -52,7 +52,6 @@ describe("always-loaded tool count", () => {
52
52
  // path would allow unchecked host command execution.
53
53
  const expectedNames = [
54
54
  "bash",
55
- "credential_store",
56
55
  "file_edit",
57
56
  "file_read",
58
57
  "file_write",
@@ -66,6 +65,6 @@ describe("always-loaded tool count", () => {
66
65
 
67
66
  expect(activeNames).toEqual(expectedNames);
68
67
  expect(activeNames.filter((name) => name === "recall")).toHaveLength(1);
69
- expect(activeTools.length).toBe(11);
68
+ expect(activeTools.length).toBe(10);
70
69
  });
71
70
  });
@@ -20,7 +20,6 @@ const ALLOWLIST = new Set([
20
20
  "assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
21
21
  "assistant/src/daemon/app-source-watcher.ts", // uses getAppsDir for recursive fs.watch root, not per-app paths
22
22
  "assistant/src/tools/filesystem/write.ts", // uses getAppsDir as an exemption root for the artifact-HTML guard, not per-app paths
23
- "assistant/src/memory/preloaded-apps.ts", // seeds fixed-slug apps (id === dirName by construction) and writes the definition JSON, which app-store has no fixed-id helper for
24
23
  ]);
25
24
 
26
25
  function isTestFile(filePath: string): boolean {
@@ -60,10 +60,7 @@ const CANONICAL_KEY_RE = /^[a-z0-9][a-z0-9-]*$/;
60
60
  * Keep this list minimal — only files that genuinely need to reference
61
61
  * the legacy format for backward compatibility.
62
62
  */
63
- const LEGACY_KEY_ALLOWLIST = new Set([
64
- // macOS client: fallback reads from legacy config section
65
- "clients/macos/vellum-assistant/Features/Settings/SettingsAccountTab.swift",
66
- ]);
63
+ const LEGACY_KEY_ALLOWLIST = new Set<string>([]);
67
64
 
68
65
  function isTestFile(filePath: string): boolean {
69
66
  return (
@@ -58,8 +58,6 @@ function loadRegistry(): Registry {
58
58
  const LEGACY_KEY_ALLOWLIST = new Set([
59
59
  // Type definitions documenting the legacy format
60
60
  "assistant/src/config/types.ts",
61
- // macOS client: fallback reads from legacy config section
62
- "clients/macos/vellum-assistant/Features/Settings/SettingsAccountTab.swift",
63
61
  ]);
64
62
 
65
63
  function isTestFile(filePath: string): boolean {
@@ -4,10 +4,7 @@ import { SYNC_TAGS } from "../daemon/message-types/sync.js";
4
4
  import type { AssistantEvent } from "../runtime/assistant-event.js";
5
5
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
6
6
  import { ROUTES as AVATAR_ROUTES } from "../runtime/routes/avatar-routes.js";
7
- import {
8
- publishIdentityChanged,
9
- publishIdentityIntroChanged,
10
- } from "../runtime/sync/resource-sync-events.js";
7
+ import { publishIdentityChanged } from "../runtime/sync/resource-sync-events.js";
11
8
 
12
9
  async function waitFor(predicate: () => boolean): Promise<void> {
13
10
  const deadline = Date.now() + 500;
@@ -81,29 +78,7 @@ describe("avatar and identity sync events", () => {
81
78
  });
82
79
  expect(received[1].message).toEqual({
83
80
  type: "sync_changed",
84
- tags: [SYNC_TAGS.assistantIdentity, SYNC_TAGS.assistantIdentityIntro],
85
- });
86
- } finally {
87
- subscription.dispose();
88
- }
89
- });
90
-
91
- test("identity intro changes emit only the identity intro sync tag", async () => {
92
- const received: AssistantEvent[] = [];
93
- const subscription = assistantEventHub.subscribe({
94
- type: "process",
95
- callback: (event) => {
96
- received.push(event);
97
- },
98
- });
99
-
100
- try {
101
- publishIdentityIntroChanged();
102
- await waitFor(() => received.length === 1);
103
-
104
- expect(received[0].message).toEqual({
105
- type: "sync_changed",
106
- tags: [SYNC_TAGS.assistantIdentityIntro],
81
+ tags: [SYNC_TAGS.assistantIdentity],
107
82
  });
108
83
  } finally {
109
84
  subscription.dispose();
@@ -78,10 +78,8 @@ mock.module("../prompts/persona-resolver.js", () => ({
78
78
  resolveUserSlug: () => null,
79
79
  }));
80
80
 
81
- mock.module("../runtime/routes/identity-intro-cache.js", () => ({
82
- getCachedIntro: () => null,
83
- readWorkspaceIdentityIntro: () => null,
84
- setCachedIntro: () => {},
81
+ mock.module("../runtime/routes/workspace-greetings.js", () => ({
82
+ readWorkspaceGreetings: () => null,
85
83
  }));
86
84
 
87
85
  // Mock getOrCreateConversation from conversation-store so the handler
@@ -345,13 +343,13 @@ describe("POST /v1/btw", () => {
345
343
  expect(options!.config!.callSite).toBe("emptyStateGreeting");
346
344
  });
347
345
 
348
- test("identity intro requests pass callSite: 'identityIntro'", async () => {
346
+ test("generic requests pass the default callSite", async () => {
349
347
  const provider = makeMockProvider();
350
348
  const session = makeMockSession(provider);
351
349
  mockGetOrCreateConversation.mockImplementationOnce(async () => session);
352
350
 
353
351
  const { result } = await callHandler({
354
- conversationKey: "identity-intro",
352
+ conversationKey: "profile-intro",
355
353
  content: "Generate an intro",
356
354
  });
357
355
  await readStream(result as ReadableStream<Uint8Array>);
@@ -361,7 +359,7 @@ describe("POST /v1/btw", () => {
361
359
  expect(options!.config!.callSite).toBe("identityIntro");
362
360
  });
363
361
 
364
- test("identity intro requests do not synthesize a static name greeting", async () => {
362
+ test("generic requests do not synthesize a static name greeting", async () => {
365
363
  const identityPath = join(getWorkspaceDir(), "IDENTITY.md");
366
364
  writeFileSync(
367
365
  identityPath,
@@ -375,7 +373,7 @@ describe("POST /v1/btw", () => {
375
373
  mockGetOrCreateConversation.mockImplementationOnce(async () => session);
376
374
 
377
375
  const { result } = await callHandler({
378
- conversationKey: "identity-intro",
376
+ conversationKey: "profile-intro",
379
377
  content: "Generate an intro",
380
378
  });
381
379
  const text = await readStream(result as ReadableStream<Uint8Array>);
@@ -790,9 +790,6 @@ describe("Permission Checker", () => {
790
790
  const workingDir = join(homedir(), "projects", "myapp");
791
791
  expect(generateScopeOptions(workingDir, "web_fetch")).toHaveLength(0);
792
792
  expect(generateScopeOptions(workingDir, "skill_load")).toHaveLength(0);
793
- expect(generateScopeOptions(workingDir, "credential_store")).toHaveLength(
794
- 0,
795
- );
796
793
  expect(
797
794
  generateScopeOptions(workingDir, "computer_use_click"),
798
795
  ).toHaveLength(0);