@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.4

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 (209) hide show
  1. package/AGENTS.md +8 -2
  2. package/ARCHITECTURE.md +2 -0
  3. package/docs/architecture/memory.md +15 -0
  4. package/docs/browser-use-architecture-phase2.md +128 -56
  5. package/docs/flux-turn-detection-spike.md +11 -6
  6. package/docs/guardian-request-flow.md +35 -0
  7. package/knip.json +3 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  10. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  11. package/openapi.yaml +136 -76
  12. package/package.json +1 -1
  13. package/scripts/write-plugin-api-shim.ts +10 -0
  14. package/src/__tests__/app-control-flow.test.ts +1 -0
  15. package/src/__tests__/approval-routes-http.test.ts +2 -2
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  17. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  18. package/src/__tests__/compaction-events.test.ts +8 -10
  19. package/src/__tests__/conversation-agent-loop.test.ts +4 -1
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  21. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  22. package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
  23. package/src/__tests__/conversation-queue.test.ts +39 -62
  24. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  27. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  28. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
  30. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  31. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  36. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  37. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  38. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  39. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  40. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  41. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  42. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  43. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  44. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  45. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  46. package/src/__tests__/document-sync-tags.test.ts +0 -75
  47. package/src/__tests__/file-ops-service.test.ts +163 -30
  48. package/src/__tests__/filesystem-tools.test.ts +23 -24
  49. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  50. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  51. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  52. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  53. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  54. package/src/__tests__/media-generate-image.test.ts +131 -21
  55. package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
  56. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  57. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
  58. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  59. package/src/__tests__/qdrant-manager.test.ts +14 -1
  60. package/src/__tests__/run-due-schedules.test.ts +21 -0
  61. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  62. package/src/__tests__/schedule-routes.test.ts +23 -0
  63. package/src/__tests__/schedule-store.test.ts +17 -0
  64. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  65. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  66. package/src/__tests__/starter-task-flow.test.ts +5 -4
  67. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  68. package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
  69. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  70. package/src/__tests__/subagent-tools.test.ts +81 -101
  71. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  72. package/src/__tests__/tool-executor.test.ts +5 -1
  73. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  74. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  75. package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
  76. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  77. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  78. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  79. package/src/acp/session-manager.ts +8 -1
  80. package/src/api/events/host-file.ts +2 -2
  81. package/src/api/surfaces.ts +5 -0
  82. package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
  83. package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
  84. package/src/calls/voice-session-bridge.ts +44 -25
  85. package/src/calls/voice-triage-escalate.ts +1 -0
  86. package/src/cli/bundled-modules.ts +29 -0
  87. package/src/cli/commands/db/repair.ts +4 -8
  88. package/src/cli/commands/domain.ts +6 -3
  89. package/src/cli/commands/email.ts +6 -3
  90. package/src/cli/commands/keys.ts +8 -3
  91. package/src/cli/commands/plugins.ts +85 -36
  92. package/src/cli/commands/schedules.ts +35 -1
  93. package/src/cli/lib/bundled-marketplace.json +1 -1
  94. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  95. package/src/config/assistant-feature-flags.ts +36 -15
  96. package/src/config/balanced-model-experiment.ts +35 -0
  97. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  98. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  99. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  100. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  101. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  102. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  103. package/src/config/call-site-defaults.ts +7 -0
  104. package/src/config/default-profile-catalog.ts +96 -4
  105. package/src/config/feature-flag-registry.json +11 -11
  106. package/src/config/skills.ts +9 -2
  107. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  108. package/src/daemon/conversation-agent-loop.ts +14 -13
  109. package/src/daemon/conversation-notifiers.ts +11 -9
  110. package/src/daemon/conversation-process.ts +0 -27
  111. package/src/daemon/conversation-runtime-assembly.ts +9 -2
  112. package/src/daemon/conversation-store.ts +4 -4
  113. package/src/daemon/conversation-surfaces.ts +27 -13
  114. package/src/daemon/conversation-tool-setup.ts +2 -4
  115. package/src/daemon/conversation.ts +104 -51
  116. package/src/daemon/doordash-steps.ts +2 -2
  117. package/src/daemon/lifecycle.ts +14 -1
  118. package/src/daemon/process-message.ts +0 -13
  119. package/src/daemon/windows-compiled-entry.ts +4 -0
  120. package/src/documents/document-store.ts +5 -235
  121. package/src/hooks/types.ts +5 -0
  122. package/src/ipc/gateway-flag-listener.ts +17 -3
  123. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
  124. package/src/live-voice/live-voice-manager.ts +16 -3
  125. package/src/live-voice/live-voice-session.ts +63 -9
  126. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  127. package/src/monitoring/control.ts +1 -0
  128. package/src/monitoring/db-integrity-sample.ts +4 -5
  129. package/src/notifications/AGENTS.md +2 -0
  130. package/src/notifications/approval-card-data.ts +33 -0
  131. package/src/permissions/prompter.ts +1 -5
  132. package/src/persistence/conversation-queries.ts +66 -16
  133. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  134. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  135. package/src/persistence/schema/documents.ts +4 -4
  136. package/src/plugin-api/constants.ts +8 -0
  137. package/src/plugin-api/index.ts +5 -1
  138. package/src/plugin-api/webhook-url.ts +13 -11
  139. package/src/plugins/defaults/main.ts +6 -7
  140. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  141. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  142. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  143. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  144. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  145. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  146. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  147. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  148. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  149. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  150. package/src/plugins/mtime-cache.ts +17 -0
  151. package/src/prompts/templates/system-sections.ts +0 -7
  152. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  153. package/src/providers/__tests__/retry-callsite.test.ts +20 -0
  154. package/src/providers/openai/chat-completions-provider.ts +11 -1
  155. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
  156. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
  157. package/src/routes/control.ts +1 -0
  158. package/src/routes/route-host-client.ts +1 -0
  159. package/src/runtime/AGENTS.md +16 -17
  160. package/src/runtime/agent-wake.ts +15 -12
  161. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
  162. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  163. package/src/runtime/routes/conversation-list-routes.ts +54 -22
  164. package/src/runtime/routes/conversation-management-routes.ts +2 -3
  165. package/src/runtime/routes/conversation-routes.ts +11 -13
  166. package/src/runtime/routes/documents-routes.ts +3 -222
  167. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  168. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  169. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  170. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  171. package/src/runtime/routes/schedule-routes.ts +94 -7
  172. package/src/runtime/routes/workspace-routes.ts +0 -9
  173. package/src/runtime/routes/workspace-utils.ts +3 -13
  174. package/src/runtime/services/conversation-serializer.ts +7 -2
  175. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  176. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  177. package/src/schedule/plugin-schedule-availability.ts +58 -0
  178. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  179. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  180. package/src/schedule/schedule-store.ts +5 -1
  181. package/src/schedule/scheduler.ts +9 -4
  182. package/src/schedule/worker-control.ts +1 -0
  183. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  184. package/src/subagent/consult-context.ts +11 -11
  185. package/src/subagent/consult-prompt.ts +26 -35
  186. package/src/subagent/manager.ts +20 -37
  187. package/src/subagent/notify.ts +7 -1
  188. package/src/subagent/types.ts +15 -13
  189. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  190. package/src/tools/acp/spawn.ts +6 -4
  191. package/src/tools/filesystem/read.ts +27 -10
  192. package/src/tools/host-filesystem/read.ts +27 -15
  193. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  194. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  195. package/src/tools/shared/filesystem/types.ts +5 -5
  196. package/src/tools/skills/scaffold-managed.ts +25 -7
  197. package/src/tools/subagent/spawn.ts +28 -88
  198. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  199. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  200. package/src/util/logger.ts +16 -0
  201. package/src/util/worker-process.ts +37 -4
  202. package/src/windows-compiled-cli.ts +32 -0
  203. package/src/windows-compiled-entry.ts +4 -0
  204. package/src/windows-compiled-logger.ts +6 -0
  205. package/src/windows-compiled-worker-entry.ts +29 -0
  206. package/src/__tests__/document-workspace-file.test.ts +0 -467
  207. package/src/daemon/interactive-turn-sender.ts +0 -59
  208. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  209. package/src/subagent/consult-transcript.ts +0 -90
@@ -125,16 +125,19 @@ describe("FileSystemOps symlink handling", () => {
125
125
  });
126
126
 
127
127
  // ===========================================================================
128
- // FileSystemOps: read offset/limit edge cases
128
+ // FileSystemOps: read character-window edge cases
129
129
  // ===========================================================================
130
130
 
131
- describe("FileSystemOps read offset/limit edge cases", () => {
132
- test("offset beyond file length returns empty content", async () => {
131
+ describe("FileSystemOps read character-window edge cases", () => {
132
+ test("startIndex beyond file length returns empty content", async () => {
133
133
  const dir = makeTempDir();
134
134
  writeFileSync(join(dir, "short.txt"), "a\nb\nc");
135
135
  const ops = new FileSystemOps(sandboxPolicyFor(dir));
136
136
 
137
- const result = await ops.readFileSafe({ path: "short.txt", offset: 100 });
137
+ const result = await ops.readFileSafe({
138
+ path: "short.txt",
139
+ startIndex: 100,
140
+ });
138
141
  expect(result.ok).toBe(true);
139
142
  if (!result.ok) {
140
143
  return;
@@ -142,12 +145,12 @@ describe("FileSystemOps read offset/limit edge cases", () => {
142
145
  expect(result.value.content).toBe("");
143
146
  });
144
147
 
145
- test("limit of zero returns empty content", async () => {
148
+ test("maxChars of zero returns empty content", async () => {
146
149
  const dir = makeTempDir();
147
150
  writeFileSync(join(dir, "file.txt"), "a\nb\nc");
148
151
  const ops = new FileSystemOps(sandboxPolicyFor(dir));
149
152
 
150
- const result = await ops.readFileSafe({ path: "file.txt", limit: 0 });
153
+ const result = await ops.readFileSafe({ path: "file.txt", maxChars: 0 });
151
154
  expect(result.ok).toBe(true);
152
155
  if (!result.ok) {
153
156
  return;
@@ -155,61 +158,57 @@ describe("FileSystemOps read offset/limit edge cases", () => {
155
158
  expect(result.value.content).toBe("");
156
159
  });
157
160
 
158
- test("offset=1 reads from first line (1-indexed)", async () => {
161
+ test("startIndex=0 reads from the first character (0-indexed)", async () => {
159
162
  const dir = makeTempDir();
160
163
  writeFileSync(join(dir, "file.txt"), "first\nsecond\nthird");
161
164
  const ops = new FileSystemOps(sandboxPolicyFor(dir));
162
165
 
163
166
  const result = await ops.readFileSafe({
164
167
  path: "file.txt",
165
- offset: 1,
166
- limit: 1,
168
+ startIndex: 0,
169
+ maxChars: 5,
167
170
  });
168
171
  expect(result.ok).toBe(true);
169
172
  if (!result.ok) {
170
173
  return;
171
174
  }
172
- expect(result.value.content).toContain("first");
173
- expect(result.value.content).not.toContain("second");
175
+ const [body] = result.value.content.split("\n\n[Truncated:");
176
+ expect(body).toBe("first");
174
177
  });
175
178
 
176
- test("limit exceeding file length returns all remaining lines", async () => {
179
+ test("maxChars exceeding file length returns the rest of the file", async () => {
177
180
  const dir = makeTempDir();
178
181
  writeFileSync(join(dir, "file.txt"), "a\nb");
179
182
  const ops = new FileSystemOps(sandboxPolicyFor(dir));
180
183
 
181
184
  const result = await ops.readFileSafe({
182
185
  path: "file.txt",
183
- offset: 1,
184
- limit: 1000,
186
+ startIndex: 0,
187
+ maxChars: 1000,
185
188
  });
186
189
  expect(result.ok).toBe(true);
187
190
  if (!result.ok) {
188
191
  return;
189
192
  }
190
- expect(result.value.content).toContain("a");
191
- expect(result.value.content).toContain("b");
193
+ expect(result.value.content).toBe("a\nb");
192
194
  });
193
195
 
194
- test("read adds line numbers starting from offset", async () => {
196
+ test("the window starts at startIndex and carries no line numbers", async () => {
195
197
  const dir = makeTempDir();
196
198
  writeFileSync(join(dir, "file.txt"), "a\nb\nc\nd\ne");
197
199
  const ops = new FileSystemOps(sandboxPolicyFor(dir));
198
200
 
199
201
  const result = await ops.readFileSafe({
200
202
  path: "file.txt",
201
- offset: 3,
202
- limit: 2,
203
+ startIndex: 4,
204
+ maxChars: 3,
203
205
  });
204
206
  expect(result.ok).toBe(true);
205
207
  if (!result.ok) {
206
208
  return;
207
209
  }
208
- // Lines should be numbered 3 and 4
209
- expect(result.value.content).toContain("3");
210
- expect(result.value.content).toContain("4");
211
- expect(result.value.content).toContain("c");
212
- expect(result.value.content).toContain("d");
210
+ const [body] = result.value.content.split("\n\n[Truncated:");
211
+ expect(body).toBe("c\nd");
213
212
  });
214
213
  });
215
214
 
@@ -83,11 +83,8 @@ function isGatewayInternal(filePath: string): boolean {
83
83
  /** Additional files allowed for the interpolated-port check only (use gateway port, not runtime). */
84
84
  const INTERPOLATED_PORT_ALLOWLIST = new Set([
85
85
  "clients/macos/src/main/bundle-flow.ts",
86
- // Electron main bridges the host proxy to a local assistant's gateway
87
- // (gatewayPort) over loopback with a Guardian-minted gateway token — same
88
- // class as bundle-flow.ts. Added with #34049 (host proxy for cloud/managed
89
- // assistants); the allowlist entry was missed there.
90
- "clients/macos/src/main/host-proxy-router.ts",
86
+ // Desktop clients connect to local assistants through their loopback gateway.
87
+ "packages/electron-desktop/src/host-proxy/router.ts",
91
88
  ]);
92
89
 
93
90
  /** Shared violation filter: exempt test files, gateway internals, and allowlisted paths. */
@@ -27,7 +27,7 @@ mock.module("../daemon/host-file-proxy.js", () => ({
27
27
  }));
28
28
 
29
29
  import { hostFileReadTool } from "../tools/host-filesystem/read.js";
30
- import { DEFAULT_READ_LINE_LIMIT } from "../tools/shared/filesystem/file-ops-service.js";
30
+ import { READ_CHAR_BUDGET } from "../tools/shared/filesystem/file-ops-service.js";
31
31
  import type { ToolContext } from "../tools/types.js";
32
32
 
33
33
  const testDirs: string[] = [];
@@ -77,19 +77,19 @@ describe("host_file_read tool", () => {
77
77
  expect(result.content).toContain("must be absolute");
78
78
  });
79
79
 
80
- test("reads file with line numbers", async () => {
80
+ test("reads a character window", async () => {
81
81
  const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
82
82
  testDirs.push(dir);
83
83
  const filePath = join(dir, "sample.txt");
84
84
  writeFileSync(filePath, "first\nsecond\nthird\n");
85
85
 
86
86
  const result = await hostFileReadTool.execute(
87
- { path: filePath, offset: 2, limit: 2 },
87
+ { path: filePath, start_index: 6, max_chars: 6 },
88
88
  makeContext(),
89
89
  );
90
90
  expect(result.isError).toBe(false);
91
- expect(result.content).toContain("2 second");
92
- expect(result.content).toContain("3 third");
91
+ expect(result.content).toContain("second");
92
+ expect(result.content).not.toContain("first");
93
93
  });
94
94
 
95
95
  test("returns error when file does not exist", async () => {
@@ -128,7 +128,7 @@ describe("host_file_read tool", () => {
128
128
  expect(result.content).toContain('Invalid input for tool "host_file_read"');
129
129
  });
130
130
 
131
- test("reads entire file when no offset or limit specified", async () => {
131
+ test("reads the whole file when no window is specified", async () => {
132
132
  const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
133
133
  testDirs.push(dir);
134
134
  const filePath = join(dir, "full.txt");
@@ -139,9 +139,7 @@ describe("host_file_read tool", () => {
139
139
  makeContext(),
140
140
  );
141
141
  expect(result.isError).toBe(false);
142
- expect(result.content).toContain("1 line1");
143
- expect(result.content).toContain("2 line2");
144
- expect(result.content).toContain("3 line3");
142
+ expect(result.content).toBe("line1\nline2\nline3\n");
145
143
  });
146
144
 
147
145
  test("handles empty file", async () => {
@@ -157,20 +155,19 @@ describe("host_file_read tool", () => {
157
155
  expect(result.isError).toBe(false);
158
156
  });
159
157
 
160
- test("offset starts from the correct line (1-indexed)", async () => {
158
+ test("start_index is 0-indexed", async () => {
161
159
  const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
162
160
  testDirs.push(dir);
163
161
  const filePath = join(dir, "lines.txt");
164
162
  writeFileSync(filePath, "a\nb\nc\nd\ne\n");
165
163
 
166
164
  const result = await hostFileReadTool.execute(
167
- { path: filePath, offset: 3, limit: 1 },
165
+ { path: filePath, start_index: 4, max_chars: 1 },
168
166
  makeContext(),
169
167
  );
170
168
  expect(result.isError).toBe(false);
171
- expect(result.content).toContain("3 c");
172
- expect(result.content).not.toContain("2 b");
173
- expect(result.content).not.toContain("4 d");
169
+ const [body] = result.content.split("\n\n[Truncated:");
170
+ expect(body).toBe("c");
174
171
  });
175
172
 
176
173
  test("reads a file with symlinks resolved", async () => {
@@ -228,15 +225,15 @@ describe("host_file_read image support", () => {
228
225
  expect(result.isError).toBe(false);
229
226
  expect(result.contentBlocks).toHaveLength(1);
230
227
  // The proxied request carries the resolved default window, so a host read
231
- // is bounded by the same limit as a local one rather than streaming a
228
+ // is bounded by the same budget as a local one rather than streaming a
232
229
  // whole file across the bridge.
233
230
  expect(requests).toEqual([
234
231
  {
235
232
  input: {
236
233
  operation: "read",
237
234
  path: "/host/screenshot.png",
238
- offset: undefined,
239
- limit: DEFAULT_READ_LINE_LIMIT,
235
+ startIndex: undefined,
236
+ maxChars: READ_CHAR_BUDGET,
240
237
  },
241
238
  conversationId: "test-conversation",
242
239
  signal: undefined,
@@ -306,8 +303,8 @@ describe("host_file_read image support", () => {
306
303
  );
307
304
 
308
305
  expect(result.isError).toBe(false);
309
- expect(result.content).toContain("1 hello world");
310
- expect(result.content).toContain("2 second line");
306
+ expect(result.content).toContain("hello world");
307
+ expect(result.content).toContain("second line");
311
308
  expect((result as any).contentBlocks).toBeUndefined();
312
309
  });
313
310
 
@@ -189,7 +189,7 @@ const _testAuthContext: AuthContext = {
189
189
  function makeConversation(overrides: Record<string, unknown> = {}) {
190
190
  return {
191
191
  setTrustContext: () => {},
192
- updateClient: () => {},
192
+ replayActivityState: () => {},
193
193
  setHostBrowserProxy: () => {},
194
194
  setHostCuProxy: () => {},
195
195
  setHostAppControlProxy: () => {},
@@ -11,6 +11,7 @@ import {
11
11
  clearFeatureFlagOverridesCache,
12
12
  initFeatureFlagOverrides,
13
13
  isAssistantFeatureFlagEnabled,
14
+ refreshOverridesFromGateway,
14
15
  } from "../config/assistant-feature-flags.js";
15
16
  import { mockGatewayIpc, resetMockGatewayIpc } from "./mock-gateway-ipc.js";
16
17
 
@@ -110,3 +111,51 @@ describe("initFeatureFlagOverrides", () => {
110
111
  expect(isAssistantFeatureFlagEnabled("second-call", config)).toBe(false);
111
112
  });
112
113
  });
114
+
115
+ describe("refreshOverridesFromGateway", () => {
116
+ it("replaces the cache with the new gateway values", async () => {
117
+ mockGatewayIpc({ "moving-flag": true });
118
+ await initFeatureFlagOverrides();
119
+
120
+ resetMockGatewayIpc();
121
+ mockGatewayIpc({ "moving-flag": false });
122
+
123
+ expect(await refreshOverridesFromGateway()).toBe(true);
124
+
125
+ const config = {} as any;
126
+ expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(false);
127
+ });
128
+
129
+ it("keeps the last known values when the refresh fails", async () => {
130
+ mockGatewayIpc({ "moving-flag": true });
131
+ await initFeatureFlagOverrides();
132
+
133
+ resetMockGatewayIpc();
134
+ mockGatewayIpc(null, { error: true });
135
+
136
+ expect(await refreshOverridesFromGateway()).toBe(false);
137
+
138
+ // A failed refresh must not drop the flag to its registry default: a
139
+ // value the assistant selects behavior from (an experiment arm, a kill
140
+ // switch) would otherwise flip on one timed-out IPC call.
141
+ const config = {} as any;
142
+ expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(true);
143
+ });
144
+
145
+ it("keeps the cache readable for the whole call", async () => {
146
+ mockGatewayIpc({ "moving-flag": true });
147
+ await initFeatureFlagOverrides();
148
+
149
+ resetMockGatewayIpc();
150
+ mockGatewayIpc({ "moving-flag": false });
151
+
152
+ const config = {} as any;
153
+ const inFlight = refreshOverridesFromGateway();
154
+ // A sync read landing mid-refresh sees the previous values, never an
155
+ // empty cache.
156
+ expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(true);
157
+
158
+ await inFlight;
159
+ expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(false);
160
+ });
161
+ });
@@ -47,6 +47,9 @@ import { skillLoadTool } from "../tools/skills/load.js";
47
47
  import { executeScaffoldManagedSkill } from "../tools/skills/scaffold-managed.js";
48
48
  import type { ToolContext } from "../tools/types.js";
49
49
 
50
+ /** scaffold_managed_skill requires activation hints; a fixed set for tests. */
51
+ const HINTS = ["user asks to run the lifecycle test procedure"];
52
+
50
53
  function makeContext(): ToolContext {
51
54
  return {
52
55
  workingDir: "/tmp",
@@ -126,6 +129,7 @@ Run the custom lifecycle verification procedure.
126
129
  name: "Lifecycle Test",
127
130
  description: "Integration test skill.",
128
131
  body_markdown: "Run the lifecycle test procedure.",
132
+ activation_hints: HINTS,
129
133
  emoji: "🧪",
130
134
  },
131
135
  makeContext(),
@@ -185,6 +189,7 @@ Run the custom lifecycle verification procedure.
185
189
  name: "V1",
186
190
  description: "Version 1.",
187
191
  body_markdown: "Original body.",
192
+ activation_hints: HINTS,
188
193
  },
189
194
  ctx,
190
195
  );
@@ -196,6 +201,7 @@ Run the custom lifecycle verification procedure.
196
201
  name: "V2",
197
202
  description: "Version 2.",
198
203
  body_markdown: "Updated body.",
204
+ activation_hints: HINTS,
199
205
  overwrite: true,
200
206
  },
201
207
  ctx,
@@ -241,6 +247,7 @@ Run the custom lifecycle verification procedure.
241
247
  description: "Created from scaffold.",
242
248
  body_markdown:
243
249
  "This skill was dynamically created.\n\nRun: `echo chain-test-ok`",
250
+ activation_hints: HINTS,
244
251
  },
245
252
  ctx,
246
253
  );
@@ -1,6 +1,14 @@
1
1
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- import { readFileSync } from "fs";
3
+ import {
4
+ mkdtempSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ symlinkSync,
9
+ writeFileSync,
10
+ } from "fs";
11
+ import { tmpdir } from "os";
4
12
  import { dirname, join } from "path";
5
13
 
6
14
  import { __resetRegistryForTesting, getTool } from "../tools/registry.js";
@@ -97,8 +105,23 @@ import { run } from "../config/bundled-skills/image-studio/tools/media-generate-
97
105
  // Clean up after this file to prevent contamination of later test files.
98
106
  afterAll(() => {
99
107
  __resetRegistryForTesting();
108
+ for (const dir of tempWorkingDirs) {
109
+ rmSync(dir, { recursive: true, force: true });
110
+ }
100
111
  });
101
112
 
113
+ const tempWorkingDirs: string[] = [];
114
+
115
+ /** Build a ToolContext with a fresh temp working directory. */
116
+ function makeContext(): ToolContext {
117
+ const workingDir = mkdtempSync(join(tmpdir(), "media-gen-test-"));
118
+ tempWorkingDirs.push(workingDir);
119
+ return {
120
+ conversationId: "conv-123",
121
+ workingDir,
122
+ } as unknown as ToolContext;
123
+ }
124
+
102
125
  const CONFIG_DIR = join(
103
126
  dirname(import.meta.dirname!),
104
127
  "config",
@@ -128,12 +151,10 @@ beforeEach(() => {
128
151
  platformBaseUrl: "",
129
152
  assistantApiKey: "",
130
153
  };
154
+ fakeContext = makeContext();
131
155
  });
132
156
 
133
- const fakeContext = {
134
- conversationId: "conv-123",
135
- workingDir: "/tmp",
136
- } as unknown as ToolContext;
157
+ let fakeContext: ToolContext;
137
158
 
138
159
  describe("image-studio skill script wrapper", () => {
139
160
  test("exports a run function without registering media_generate_image in the tool registry", async () => {
@@ -431,28 +452,117 @@ describe("image-studio skill script wrapper", () => {
431
452
  });
432
453
 
433
454
  test("reads source images from file paths on disk", async () => {
434
- // Write a temp image file inside the workspace (fakeContext.workingDir = /tmp)
435
- const tmpPath = join("/tmp", "test-source-image.png");
455
+ // Write a temp image file inside the workspace
456
+ const tmpPath = join(fakeContext.workingDir, "test-source-image.png");
436
457
  const pngBytes = Buffer.from(
437
458
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
438
459
  "base64",
439
460
  );
440
461
  await Bun.write(tmpPath, pngBytes);
441
462
 
442
- try {
443
- const result = await run(
444
- { prompt: "edit this", mode: "edit", source_paths: [tmpPath] },
445
- fakeContext,
446
- );
447
-
448
- expect(result.isError).toBe(false);
449
- expect(result.content).toContain("Generated 1 image");
450
- } finally {
451
- const { unlink } = await import("fs/promises");
452
- if (await Bun.file(tmpPath).exists()) {
453
- await unlink(tmpPath);
454
- }
455
- }
463
+ const result = await run(
464
+ { prompt: "edit this", mode: "edit", source_paths: [tmpPath] },
465
+ fakeContext,
466
+ );
467
+
468
+ expect(result.isError).toBe(false);
469
+ expect(result.content).toContain("Generated 1 image");
470
+ });
471
+
472
+ test("saves the image into media/generated and returns the embed instruction", async () => {
473
+ const result = await run({ prompt: "a sunset" }, fakeContext);
474
+
475
+ expect(result.isError).toBe(false);
476
+ expect(result.content).toContain("Saved to media/generated/a-sunset.png");
477
+ expect(result.content).toContain(
478
+ "![description](vellum://workspace/media/generated/a-sunset.png)",
479
+ );
480
+ const saved = Bun.file(
481
+ join(fakeContext.workingDir, "media/generated/a-sunset.png"),
482
+ );
483
+ expect(await saved.exists()).toBe(true);
484
+ const bytes = Buffer.from(await saved.arrayBuffer());
485
+ expect(bytes.equals(Buffer.from("generated-data", "base64"))).toBe(true);
486
+ });
487
+
488
+ test("uses the image title for the filename when present", async () => {
489
+ mockGenerateResult = {
490
+ images: [
491
+ {
492
+ mimeType: "image/png",
493
+ dataBase64: "img1",
494
+ title: "Twilight Gremlin!",
495
+ } as never,
496
+ ],
497
+ text: "",
498
+ resolvedModel: "gemini-3.1-flash-image-preview",
499
+ };
500
+
501
+ const result = await run({ prompt: "a pond creature" }, fakeContext);
502
+
503
+ expect(result.isError).toBe(false);
504
+ expect(result.content).toContain(
505
+ "Saved to media/generated/twilight-gremlin.png",
506
+ );
507
+ });
508
+
509
+ test("gives each variant a distinct filename", async () => {
510
+ mockGenerateResult = {
511
+ images: [
512
+ { mimeType: "image/png", dataBase64: "img1" },
513
+ { mimeType: "image/png", dataBase64: "img2" },
514
+ ],
515
+ text: undefined as unknown as string,
516
+ resolvedModel: "gemini-3.1-flash-image-preview",
517
+ };
518
+
519
+ const result = await run({ prompt: "a robot", variants: 2 }, fakeContext);
520
+
521
+ expect(result.isError).toBe(false);
522
+ expect(result.content).toContain("- media/generated/a-robot.png");
523
+ expect(result.content).toContain("- media/generated/a-robot-2.png");
524
+ expect(
525
+ await Bun.file(
526
+ join(fakeContext.workingDir, "media/generated/a-robot-2.png"),
527
+ ).exists(),
528
+ ).toBe(true);
529
+ });
530
+
531
+ test("suffixes the filename when a previous generation already used it", async () => {
532
+ const first = await run({ prompt: "a fox" }, fakeContext);
533
+ expect(first.content).toContain("Saved to media/generated/a-fox.png");
534
+
535
+ const second = await run({ prompt: "a fox" }, fakeContext);
536
+ expect(second.content).toContain("Saved to media/generated/a-fox-2.png");
537
+ });
538
+
539
+ test("refuses to write through a symlink that escapes the workspace", async () => {
540
+ const outside = mkdtempSync(join(tmpdir(), "media-gen-outside-"));
541
+ tempWorkingDirs.push(outside);
542
+ symlinkSync(outside, join(fakeContext.workingDir, "media"));
543
+
544
+ const result = await run({ prompt: "a cat" }, fakeContext);
545
+
546
+ expect(result.isError).toBe(false);
547
+ expect(result.content).toContain("Could not save to the workspace");
548
+ expect(result.content).not.toContain("vellum://workspace/");
549
+ expect(readdirSync(outside)).toEqual([]);
550
+ });
551
+
552
+ test("falls back to inline-only when the workspace write fails", async () => {
553
+ // Occupy the media path with a regular file so the directory creation
554
+ // under it fails.
555
+ writeFileSync(join(fakeContext.workingDir, "media"), "not a directory");
556
+
557
+ const result = await run({ prompt: "a cat" }, fakeContext);
558
+
559
+ expect(result.isError).toBe(false);
560
+ expect(result.content).toContain("Could not save to the workspace");
561
+ expect(result.content).toContain(
562
+ "will be attached to your reply automatically",
563
+ );
564
+ expect(result.content).not.toContain("vellum://workspace/");
565
+ expect(result.contentBlocks).toHaveLength(1);
456
566
  });
457
567
 
458
568
  test("returns error when all source_paths are invalid", async () => {
@@ -64,9 +64,13 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
64
64
  broadcastMessage: broadcastMessageMock,
65
65
  }));
66
66
 
67
- import type { UserPromptSubmitContext } from "@vellumai/plugin-api";
67
+ import {
68
+ type UserPromptSubmitContext,
69
+ VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
70
+ } from "@vellumai/plugin-api";
68
71
 
69
72
  import type { AssistantEvent } from "../api/index.js";
73
+ import { ESCALATION_CONTINUATION_CONTENT } from "../calls/voice-triage-escalate.js";
70
74
  import type { AssistantConfig } from "../config/schema.js";
71
75
  import type { Conversation } from "../daemon/conversation.js";
72
76
  import type { QdrantSparseVector } from "../persistence/embeddings/qdrant-client.js";
@@ -74,6 +78,7 @@ import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/c
74
78
  import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory/hooks/user-prompt-submit.js";
75
79
  import type { Message } from "../providers/types.js";
76
80
  import { asConversation } from "./helpers/mock-conversation.js";
81
+ import { setConfig } from "./helpers/set-config.js";
77
82
 
78
83
  /** Canonical metrics payload the graph retriever attaches to a real hit. */
79
84
  function makeMetrics() {
@@ -175,13 +180,14 @@ function installConversation(
175
180
  trusted?: boolean;
176
181
  abortController?: AbortController;
177
182
  },
178
- ): void {
183
+ ): Conversation {
179
184
  currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
180
185
  currentConversation = asConversation({
181
186
  graphMemory,
182
187
  trustContext: undefined,
183
188
  abortController: opts?.abortController ?? new AbortController(),
184
189
  });
190
+ return currentConversation;
185
191
  }
186
192
 
187
193
  beforeEach(() => {
@@ -192,9 +198,95 @@ beforeEach(() => {
192
198
  broadcastMessageMock.mockReset();
193
199
  currentConversation = undefined;
194
200
  currentTrustClass = "guardian";
201
+ setConfig("memory", { enabled: true, v3: { live: false } });
195
202
  });
196
203
 
197
204
  describe("user-prompt-submit hook (memory retrieval)", () => {
205
+ test("voice front door skips serial legacy retrieval", async () => {
206
+ const { memory, prepareMemoryMock, recordPkbQueryVectorsMock } =
207
+ makeFakeGraphMemory();
208
+ const conversation = installConversation(memory, { trusted: true });
209
+ conversation.currentCallSite = "voiceFrontDoor";
210
+ const ctx = makeHookCtx({ conversationId: "conv-voice" });
211
+
212
+ await userPromptSubmitMemoryRetrieval(ctx);
213
+
214
+ expect(prepareMemoryMock).not.toHaveBeenCalled();
215
+ expect(recordPkbQueryVectorsMock).toHaveBeenCalledWith(
216
+ undefined,
217
+ undefined,
218
+ );
219
+ expect(applyRuntimeInjectionsMock).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ test("voice escalation continuation routes legacy retrieval on the preceding user message", async () => {
223
+ const { memory, prepareMemoryMock } = makeFakeGraphMemory();
224
+ installConversation(memory, { trusted: true });
225
+ const latestMessages: Message[] = [
226
+ {
227
+ role: "user",
228
+ content: [
229
+ { type: "text", text: "<info>\ncarried context\n</info>" },
230
+ {
231
+ type: "text",
232
+ text: "<turn_context>\nturn context\n</turn_context>",
233
+ },
234
+ { type: "text", text: "What is my preferred editor?" },
235
+ ],
236
+ },
237
+ {
238
+ role: "assistant",
239
+ content: [{ type: "text", text: "Let me check that." }],
240
+ },
241
+ {
242
+ role: "user",
243
+ content: [{ type: "text", text: "Continue the answer." }],
244
+ },
245
+ ];
246
+ const ctx = makeHookCtx({
247
+ latestMessages,
248
+ isHiddenPrompt: true,
249
+ prompt: ESCALATION_CONTINUATION_CONTENT,
250
+ messageKind: VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
251
+ });
252
+
253
+ await userPromptSubmitMemoryRetrieval(ctx);
254
+
255
+ expect(prepareMemoryMock).toHaveBeenCalledTimes(1);
256
+ expect(prepareMemoryMock.mock.calls[0]?.[0]).toBe(latestMessages);
257
+ expect(prepareMemoryMock.mock.calls[0]?.[4]).toEqual([
258
+ {
259
+ role: "user",
260
+ content: [{ type: "text", text: "What is my preferred editor?" }],
261
+ },
262
+ ]);
263
+ });
264
+
265
+ test("generic hidden prompts route legacy retrieval on their own content", async () => {
266
+ const { memory, prepareMemoryMock } = makeFakeGraphMemory();
267
+ installConversation(memory, { trusted: true });
268
+ const latestMessages: Message[] = [
269
+ {
270
+ role: "user",
271
+ content: [{ type: "text", text: "Visible caller message" }],
272
+ },
273
+ {
274
+ role: "user",
275
+ content: [{ type: "text", text: "Hidden prompt content" }],
276
+ },
277
+ ];
278
+ const ctx = makeHookCtx({
279
+ latestMessages,
280
+ isHiddenPrompt: true,
281
+ prompt: "Hidden prompt content",
282
+ });
283
+
284
+ await userPromptSubmitMemoryRetrieval(ctx);
285
+
286
+ expect(prepareMemoryMock).toHaveBeenCalledTimes(1);
287
+ expect(prepareMemoryMock.mock.calls[0]?.[4]).toBe(latestMessages);
288
+ });
289
+
198
290
  test("adopts the injected run messages when the actor is trusted", async () => {
199
291
  const injected: Message[] = [
200
292
  { role: "user", content: [{ type: "text", text: "injected" }] },