@vellumai/assistant 0.8.9-staging.2 → 0.8.9-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 (196) hide show
  1. package/docs/activation-funnel-telemetry.md +310 -0
  2. package/openapi.yaml +16 -115
  3. package/package.json +1 -1
  4. package/src/__tests__/activation-early-marking.test.ts +120 -0
  5. package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
  6. package/src/__tests__/anthropic-provider.test.ts +23 -8
  7. package/src/__tests__/approval-cascade.test.ts +1 -1
  8. package/src/__tests__/compaction-direct.test.ts +32 -18
  9. package/src/__tests__/compaction-events.test.ts +2 -2
  10. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  11. package/src/__tests__/context-overflow-reducer.test.ts +5 -5
  12. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  13. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  14. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  15. package/src/__tests__/conversation-error.test.ts +15 -1
  16. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  17. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  18. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
  19. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  20. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  21. package/src/__tests__/conversation-queue.test.ts +1 -1
  22. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
  23. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  24. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  25. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  26. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  29. package/src/__tests__/conversation-undo.test.ts +2 -2
  30. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  31. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  32. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  33. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  34. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  35. package/src/__tests__/history-repair-hook.test.ts +2 -0
  36. package/src/__tests__/llm-resolver.test.ts +73 -0
  37. package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
  38. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  39. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  40. package/src/__tests__/plugin-external-api.test.ts +68 -0
  41. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  42. package/src/__tests__/published-app-updater.test.ts +138 -0
  43. package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
  44. package/src/__tests__/title-generate-hook.test.ts +2 -0
  45. package/src/__tests__/web-fetch.test.ts +45 -0
  46. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  47. package/src/acp/resolve-agent.test.ts +0 -56
  48. package/src/acp/resolve-agent.ts +10 -38
  49. package/src/agent/loop.ts +13 -27
  50. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  51. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  52. package/src/cli/commands/memory-v3.ts +57 -199
  53. package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
  54. package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
  55. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
  56. package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
  57. package/src/cli/lib/install-from-github.ts +258 -41
  58. package/src/cli/lib/plugin-details.ts +20 -13
  59. package/src/cli/lib/plugin-marketplace.ts +23 -5
  60. package/src/cli/lib/search-plugins.ts +14 -8
  61. package/src/config/acp-defaults.ts +3 -3
  62. package/src/config/acp-schema.ts +1 -7
  63. package/src/config/bundled-skills/acp/SKILL.md +4 -17
  64. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  65. package/src/config/call-site-defaults.ts +0 -1
  66. package/src/config/feature-flag-registry.json +3 -18
  67. package/src/config/llm-resolver.ts +39 -7
  68. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  69. package/src/config/schemas/call-site-catalog.ts +0 -7
  70. package/src/config/schemas/llm.ts +17 -1
  71. package/src/config/schemas/memory-v3.ts +58 -8
  72. package/src/config/seed-inference-profiles.ts +18 -0
  73. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  74. package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
  75. package/src/daemon/conversation-agent-loop.ts +87 -95
  76. package/src/daemon/conversation-error.ts +31 -4
  77. package/src/daemon/conversation-history.ts +1 -1
  78. package/src/daemon/conversation-media-retry.ts +19 -6
  79. package/src/daemon/conversation-messaging.ts +17 -0
  80. package/src/daemon/conversation-process.ts +14 -5
  81. package/src/daemon/conversation-queue-manager.ts +8 -0
  82. package/src/daemon/conversation-runtime-assembly.ts +37 -1
  83. package/src/daemon/conversation-surfaces.ts +141 -3
  84. package/src/daemon/conversation.ts +48 -13
  85. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  86. package/src/daemon/persist-unsendable-image.ts +62 -25
  87. package/src/daemon/process-message.ts +1 -1
  88. package/src/daemon/tool-side-effects.ts +15 -0
  89. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  90. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  91. package/src/memory/activation-session-store.ts +43 -0
  92. package/src/memory/db-init.ts +4 -0
  93. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  94. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  95. package/src/memory/migrations/index.ts +2 -0
  96. package/src/memory/onboarding-events-store.ts +66 -18
  97. package/src/memory/schema/infrastructure.ts +13 -0
  98. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  99. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  100. package/src/memory/v2/consolidation-job.ts +2 -72
  101. package/src/memory/v2/types.ts +5 -0
  102. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  103. package/src/notifications/adapters/telegram.ts +7 -1
  104. package/src/plugin-api/constants.ts +2 -2
  105. package/src/plugin-api/index.ts +2 -2
  106. package/src/plugin-api/types.ts +19 -5
  107. package/src/plugins/defaults/compaction/compact.ts +24 -15
  108. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
  109. package/src/plugins/defaults/compaction/manager-store.ts +1 -1
  110. package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
  111. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
  112. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  113. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  114. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  115. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  116. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  117. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  118. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  119. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  120. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  121. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  122. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  123. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  124. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  125. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  126. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  127. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  128. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  129. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  130. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  131. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  132. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  133. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  134. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  135. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  136. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  137. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  138. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  139. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  140. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  141. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  142. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  143. package/src/plugins/external-api.ts +114 -0
  144. package/src/prompts/system-prompt.ts +61 -10
  145. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
  146. package/src/providers/anthropic/client.ts +9 -10
  147. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  148. package/src/providers/inference/logit-bias.ts +55 -0
  149. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  150. package/src/providers/openai/chat-completions-provider.ts +44 -1
  151. package/src/providers/retry.ts +22 -0
  152. package/src/providers/types.ts +6 -0
  153. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  154. package/src/runtime/routes/acp-routes.test.ts +3 -20
  155. package/src/runtime/routes/app-management-routes.ts +3 -0
  156. package/src/runtime/routes/conversation-routes.ts +2 -0
  157. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  158. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  159. package/src/runtime/routes/stt-routes.ts +45 -12
  160. package/src/runtime/routes/workspace-routes.ts +50 -15
  161. package/src/services/published-app-updater.ts +30 -8
  162. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  163. package/src/telemetry/activation-funnel.ts +167 -0
  164. package/src/telemetry/types.ts +13 -0
  165. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  166. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  167. package/src/tools/acp/list-agents.test.ts +2 -18
  168. package/src/tools/acp/list-agents.ts +3 -15
  169. package/src/tools/acp/spawn.test.ts +0 -10
  170. package/src/tools/browser/browser-execution.ts +12 -2
  171. package/src/tools/network/web-fetch.ts +65 -24
  172. package/src/tools/skills/load.ts +1 -1
  173. package/src/tools/ui-surface/definitions.ts +7 -0
  174. package/src/acp/feature-gate.test.ts +0 -48
  175. package/src/acp/feature-gate.ts +0 -34
  176. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  177. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  178. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  179. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  180. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  181. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  182. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  183. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  184. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  185. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  186. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  187. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  188. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  189. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  190. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  191. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  192. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  193. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  194. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  195. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  196. package/src/util/map-limit.ts +0 -27
@@ -1,3 +1,4 @@
1
+ import { optimizeImageForTransport } from "../../agent/image-optimize.js";
1
2
  import { getConfig } from "../../config/loader.js";
2
3
  import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
3
4
  import type { ImageContent } from "../../providers/types.js";
@@ -1206,13 +1207,22 @@ export async function executeBrowserScreenshot(
1206
1207
  { quality: 80, fullPage },
1207
1208
  context.signal,
1208
1209
  );
1209
- const base64Data = buffer.toString("base64");
1210
+ const rawBase64 = buffer.toString("base64");
1211
+
1212
+ // Downscale before handing the screenshot to the model. A full-page
1213
+ // capture of a tall or high-DPI page can exceed Anthropic's 5 MB
1214
+ // per-image cap, which would otherwise persist into the conversation
1215
+ // history inside this tool_result and reject every subsequent turn.
1216
+ const { data: base64Data, mediaType } = optimizeImageForTransport(
1217
+ rawBase64,
1218
+ "image/jpeg",
1219
+ );
1210
1220
 
1211
1221
  const imageBlock: ImageContent = {
1212
1222
  type: "image" as const,
1213
1223
  source: {
1214
1224
  type: "base64" as const,
1215
- media_type: "image/jpeg",
1225
+ media_type: mediaType,
1216
1226
  data: base64Data,
1217
1227
  },
1218
1228
  };
@@ -152,25 +152,56 @@ function isNullBodyStatus(status: number): boolean {
152
152
  return status === 204 || status === 205 || status === 304;
153
153
  }
154
154
 
155
+ // The WHATWG `Response` constructor only accepts 101 or the range 200–599 and
156
+ // throws a `RangeError` for anything else. Some servers — notably anti-bot
157
+ // gateways like LinkedIn and various job boards — reply with non-standard codes
158
+ // such as 999. Map those onto 502 so we can still build a Response; the real
159
+ // upstream status is preserved via `upstreamStatusByResponse` for reporting.
160
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Response/Response
161
+ const RESPONSE_STATUS_SWITCHING_PROTOCOLS = 101;
162
+ const RESPONSE_STATUS_MIN = 200;
163
+ const RESPONSE_STATUS_MAX = 599;
164
+
165
+ function toConstructableStatus(status: number): number {
166
+ if (status === RESPONSE_STATUS_SWITCHING_PROTOCOLS) return status;
167
+ if (status >= RESPONSE_STATUS_MIN && status <= RESPONSE_STATUS_MAX) {
168
+ return status;
169
+ }
170
+ return 502;
171
+ }
172
+
173
+ const upstreamStatusByResponse = new WeakMap<Response, number>();
174
+
175
+ // Returns the real status the upstream server sent, which may fall outside the
176
+ // range the `Response` object can represent (e.g. 999). Falls back to
177
+ // `response.status` for responses built by native `fetch`, whose undici
178
+ // implementation already exposes non-standard codes directly.
179
+ export function getUpstreamStatus(response: Response): number {
180
+ return upstreamStatusByResponse.get(response) ?? response.status;
181
+ }
182
+
155
183
  export function buildFetchResponseFromNodeResponse(
156
184
  res: NodeHttpResponseLike,
157
185
  ): Response {
158
- const status = res.statusCode ?? 502;
186
+ const upstreamStatus = res.statusCode ?? 502;
187
+ const status = toConstructableStatus(upstreamStatus);
159
188
  const responseHeaders = buildResponseHeaders(res.headers);
160
189
  const statusText = res.statusMessage ?? "";
161
190
 
162
- if (isNullBodyStatus(status)) {
163
- // Drain any unexpected bytes and produce a valid null-body fetch Response.
164
- res.resume();
165
- return new Response(null, { status, statusText, headers: responseHeaders });
166
- }
191
+ const response = isNullBodyStatus(status)
192
+ ? // Drain any unexpected bytes and produce a valid null-body fetch Response.
193
+ (res.resume(),
194
+ new Response(null, { status, statusText, headers: responseHeaders }))
195
+ : new Response(Readable.toWeb(res) as unknown as BodyInit, {
196
+ status,
197
+ statusText,
198
+ headers: responseHeaders,
199
+ });
167
200
 
168
- const body = Readable.toWeb(res);
169
- return new Response(body as unknown as BodyInit, {
170
- status,
171
- statusText,
172
- headers: responseHeaders,
173
- });
201
+ if (status !== upstreamStatus) {
202
+ upstreamStatusByResponse.set(response, upstreamStatus);
203
+ }
204
+ return response;
174
205
  }
175
206
 
176
207
  function createAbortError(): Error {
@@ -252,7 +283,16 @@ const defaultRequestExecutor: WebFetchRequestExecutor = async (
252
283
 
253
284
  return await new Promise<Response>((resolve, reject) => {
254
285
  const req = requestFn(requestOptions, (res) => {
255
- resolve(buildFetchResponseFromNodeResponse(res));
286
+ // Building the fetch Response runs inside this HTTP `response` event
287
+ // callback. Any synchronous throw here would escape as an uncaught
288
+ // exception and crash the daemon, so funnel failures into `reject` to
289
+ // keep them local to this request.
290
+ try {
291
+ resolve(buildFetchResponseFromNodeResponse(res));
292
+ } catch (err) {
293
+ res.resume();
294
+ reject(err);
295
+ }
256
296
  });
257
297
  req.once("error", reject);
258
298
  req.end();
@@ -730,9 +770,10 @@ export async function executeWebFetch(
730
770
  );
731
771
  }
732
772
 
773
+ const upstreamStatus = getUpstreamStatus(response);
733
774
  const location = response.headers.get("location");
734
775
  const isRedirect =
735
- response.status >= 300 && response.status < 400 && !!location;
776
+ upstreamStatus >= 300 && upstreamStatus < 400 && !!location;
736
777
  if (!isRedirect) break;
737
778
 
738
779
  if (redirectCount >= MAX_REDIRECTS) {
@@ -741,7 +782,7 @@ export async function executeWebFetch(
741
782
  {
742
783
  url: requestedUrl,
743
784
  finalUrl: currentUrl.href,
744
- status: response.status,
785
+ status: getUpstreamStatus(response),
745
786
  redirectCount,
746
787
  },
747
788
  );
@@ -761,7 +802,7 @@ export async function executeWebFetch(
761
802
  {
762
803
  url: requestedUrl,
763
804
  finalUrl: currentUrl.href,
764
- status: response.status,
805
+ status: getUpstreamStatus(response),
765
806
  redirectCount,
766
807
  },
767
808
  );
@@ -773,7 +814,7 @@ export async function executeWebFetch(
773
814
  {
774
815
  url: requestedUrl,
775
816
  finalUrl: currentUrl.href,
776
- status: response.status,
817
+ status: getUpstreamStatus(response),
777
818
  redirectCount,
778
819
  },
779
820
  );
@@ -785,7 +826,7 @@ export async function executeWebFetch(
785
826
  {
786
827
  url: requestedUrl,
787
828
  finalUrl: currentUrl.href,
788
- status: response.status,
829
+ status: getUpstreamStatus(response),
789
830
  redirectCount,
790
831
  },
791
832
  );
@@ -805,7 +846,7 @@ export async function executeWebFetch(
805
846
  {
806
847
  url: requestedUrl,
807
848
  finalUrl: currentUrl.href,
808
- status: response.status,
849
+ status: getUpstreamStatus(response),
809
850
  redirectCount,
810
851
  },
811
852
  );
@@ -817,7 +858,7 @@ export async function executeWebFetch(
817
858
  {
818
859
  url: requestedUrl,
819
860
  finalUrl: currentUrl.href,
820
- status: response.status,
861
+ status: getUpstreamStatus(response),
821
862
  redirectCount,
822
863
  },
823
864
  );
@@ -844,7 +885,7 @@ export async function executeWebFetch(
844
885
  {
845
886
  url: requestedUrl,
846
887
  finalUrl: currentUrl.href,
847
- status: response.status,
888
+ status: getUpstreamStatus(response),
848
889
  contentType,
849
890
  redirectCount,
850
891
  },
@@ -909,7 +950,7 @@ export async function executeWebFetch(
909
950
  const content = formatWebFetchOutput({
910
951
  requestedUrl: safeRequestedUrl,
911
952
  finalUrl: sanitizeUrlForOutput(currentUrl),
912
- status: response.status,
953
+ status: getUpstreamStatus(response),
913
954
  statusText: response.statusText,
914
955
  contentType,
915
956
  bytesRead: body.bytesRead,
@@ -931,7 +972,7 @@ export async function executeWebFetch(
931
972
  const meta: WebFetchMetadata = {
932
973
  url: safeRequestedUrl,
933
974
  finalUrl: sanitizeUrlForOutput(currentUrl),
934
- status: response.status,
975
+ status: getUpstreamStatus(response),
935
976
  contentType: contentType || undefined,
936
977
  byteCount: body.bytesRead,
937
978
  charCount: sliced.length,
@@ -945,7 +986,7 @@ export async function executeWebFetch(
945
986
  };
946
987
 
947
988
  if (!response.ok) {
948
- const errorMessage = `Error: HTTP ${response.status}`;
989
+ const errorMessage = `Error: HTTP ${getUpstreamStatus(response)}`;
949
990
  return {
950
991
  content: `${errorMessage}\n\n${content}`,
951
992
  isError: true,
@@ -128,7 +128,7 @@ export const skillLoadTool = {
128
128
  name: "skill_load",
129
129
 
130
130
  description:
131
- 'Load full instructions for a skill. Works for both bundled skills (listed in the catalog) and custom workspace skills. For app, website, dashboard, game, calculator, tracker, visualization, or interactive tool requests, load `app-builder` with `skill: "app-builder"`.',
131
+ 'Load full instructions for a skill. Works for both bundled skills (listed in the catalog) and custom workspace skills. Bundled/first-party skills (like `app-builder`) are already installed — loading only activates their instructions for the current conversation, since skills unload between turns; treat "load" as activation, not installation, and don\'t tell users a bundled skill needs installing. Loading can still fail (e.g. a feature-gated skill returns "currently unavailable", or the load errors) — if it does, relay that specific error rather than claiming the skill is not installed or offering to install it. For app, website, dashboard, game, calculator, tracker, visualization, or interactive tool requests, load `app-builder` with `skill: "app-builder"`.',
132
132
 
133
133
  category: "skills",
134
134
 
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { RiskLevel } from "../../permissions/types.js";
11
+ import { ACTIVATION_MOMENT_PARAMS } from "../../telemetry/activation-funnel.js";
11
12
  import type {
12
13
  ToolContext,
13
14
  ToolDefinition,
@@ -204,6 +205,12 @@ export const uiShowTool = {
204
205
  description:
205
206
  "When true, clicking an action does not dismiss the surface — the card stays visible and only the clicked action is marked as spent. Use for launcher or menu-style cards where multiple buttons may be clicked. Defaults to false.",
206
207
  },
208
+ activation_moment: {
209
+ type: "string",
210
+ enum: ACTIVATION_MOMENT_PARAMS,
211
+ description:
212
+ "Activation-rail telemetry tag (cohort only). Set this when this surface IS one of the activation funnel moments; the milestone is recorded automatically when the user commits the surface. Omit for all non-activation surfaces.",
213
+ },
207
214
  },
208
215
  required: ["surface_type", "data"],
209
216
  },
@@ -1,48 +0,0 @@
1
- /**
2
- * Tests for the ACP feature gate.
3
- *
4
- * `isAcpEnabled` is an OR of the `acp` feature flag and the legacy
5
- * `config.acp.enabled` field: either switch enables the subsystem, and
6
- * neither implicitly flips the other.
7
- */
8
-
9
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
-
11
- import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
12
- import type { AssistantConfig } from "../config/schema.js";
13
- import { ACP_FLAG_KEY, isAcpEnabled } from "./feature-gate.js";
14
-
15
- beforeEach(() => {
16
- setOverridesForTesting({});
17
- });
18
-
19
- afterEach(() => {
20
- setOverridesForTesting({});
21
- });
22
-
23
- /** Minimal AssistantConfig carrying only the `acp` section the gate reads. */
24
- function makeConfig(acpEnabled: boolean): AssistantConfig {
25
- return {
26
- acp: { enabled: acpEnabled, maxConcurrentSessions: 4, agents: {} },
27
- } as AssistantConfig;
28
- }
29
-
30
- describe("isAcpEnabled", () => {
31
- test("returns false when both the flag and config.acp.enabled are off", () => {
32
- expect(isAcpEnabled(makeConfig(false))).toBe(false);
33
- });
34
-
35
- test("returns true when config.acp.enabled is true and the flag is off", () => {
36
- expect(isAcpEnabled(makeConfig(true))).toBe(true);
37
- });
38
-
39
- test("returns true when the flag is on and config.acp.enabled is false", () => {
40
- setOverridesForTesting({ [ACP_FLAG_KEY]: true });
41
- expect(isAcpEnabled(makeConfig(false))).toBe(true);
42
- });
43
-
44
- test("explicit flag-off override does not defeat config.acp.enabled", () => {
45
- setOverridesForTesting({ [ACP_FLAG_KEY]: false });
46
- expect(isAcpEnabled(makeConfig(true))).toBe(true);
47
- });
48
- });
@@ -1,34 +0,0 @@
1
- /**
2
- * ACP (Agent Client Protocol) feature gate.
3
- *
4
- * Single source of truth for whether the ACP subsystem is enabled. Modeled
5
- * on `credential-execution/feature-gates.ts`: the flag key is declared in
6
- * `meta/feature-flags/feature-flag-registry.json` and resolved through the
7
- * unified feature-flag resolver.
8
- *
9
- * The gate is an OR of two independent switches:
10
- * 1. `config.acp.enabled` — the original workspace config field. Preserved
11
- * so existing workspaces with `acp.enabled: true` keep working without
12
- * any migration.
13
- * 2. The `acp` feature flag — adds a UI toggle. Flag state is persisted by
14
- * the gateway and hot-refreshed in the daemon via the
15
- * `feature_flags_changed` SSE event, so toggling it takes effect
16
- * without a restart or config-file edit.
17
- */
18
-
19
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
20
- import type { AssistantConfig } from "../config/schema.js";
21
-
22
- /** Gate for the ACP coding-agent subsystem (must match the registry). */
23
- export const ACP_FLAG_KEY = "acp" as const;
24
-
25
- /**
26
- * Whether ACP agent spawning/steering is enabled, via either the legacy
27
- * `acp.enabled` config field or the `acp` feature flag (see module doc).
28
- */
29
- export function isAcpEnabled(config: AssistantConfig): boolean {
30
- return (
31
- config.acp?.enabled === true ||
32
- isAssistantFeatureFlagEnabled(ACP_FLAG_KEY, config)
33
- );
34
- }
@@ -1,242 +0,0 @@
1
- /**
2
- * Tests for the v3 page→leaf classifier. Uses a stub provider so no live LLM
3
- * is hit. Pages are written into a temp workspace.
4
- */
5
-
6
- import { mkdtemp, rm } from "node:fs/promises";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
-
11
- import { readPage, writePage } from "../../../../memory/v2/page-store.js";
12
- import type { ConceptPageFrontmatter } from "../../../../memory/v2/types.js";
13
- import type {
14
- Message,
15
- Provider,
16
- ProviderResponse,
17
- SendMessageOptions,
18
- ToolUseContent,
19
- } from "../../../../providers/types.js";
20
- import type { LeafNode, LeafTree } from "../types.js";
21
-
22
- // Stub the provider layer before importing `assign.js` so its static import
23
- // chain (which transitively pulls in CES/credential workspace packages that
24
- // aren't built in a fresh worktree) never loads. The classifier is
25
- // provider-injectable, so every test passes its own stub provider — the real
26
- // `getConfiguredProvider` is never exercised here.
27
- mock.module("../../../../providers/provider-send-message.js", () => ({
28
- getConfiguredProvider: async () => null,
29
- extractToolUse: (response: ProviderResponse) =>
30
- response.content.find((b): b is ToolUseContent => b.type === "tool_use"),
31
- userMessage: (text: string): Message => ({
32
- role: "user",
33
- content: [{ type: "text", text }],
34
- }),
35
- }));
36
-
37
- const { assignPages } = await import("../assign.js");
38
-
39
- function makeLeaf(path: string, description: string): LeafNode {
40
- return {
41
- path,
42
- frontmatter: { path, in_core: false },
43
- description,
44
- members: [],
45
- domain: path.split("/")[0],
46
- };
47
- }
48
-
49
- function makeTree(leaves: LeafNode[]): LeafTree {
50
- return {
51
- leaves: new Map(leaves.map((l) => [l.path, l])),
52
- byPage: new Map(),
53
- };
54
- }
55
-
56
- /** Sorted by path: [1] domain-a/topic-x, [2] domain-a/topic-y, [3] domain-b/topic-z */
57
- function threeLeafTree(): LeafTree {
58
- return makeTree([
59
- makeLeaf("domain-b/topic-z", "about z"),
60
- makeLeaf("domain-a/topic-x", "about x"),
61
- makeLeaf("domain-a/topic-y", "about y"),
62
- ]);
63
- }
64
-
65
- function stubProvider(
66
- handler: (
67
- messages: Message[],
68
- options?: SendMessageOptions,
69
- ) => ProviderResponse | Promise<ProviderResponse>,
70
- ): Provider {
71
- return {
72
- name: "stub",
73
- sendMessage: async (messages, options) => handler(messages, options),
74
- } as Provider;
75
- }
76
-
77
- function response(content: ProviderResponse["content"]): ProviderResponse {
78
- return {
79
- content,
80
- model: "stub-model",
81
- stopReason: "tool_use",
82
- usage: { inputTokens: 0, outputTokens: 0 },
83
- };
84
- }
85
-
86
- function toolResponse(ids: unknown[]): ProviderResponse {
87
- return response([
88
- { type: "tool_use", id: "tu-1", name: "assign_leaves", input: { ids } },
89
- ]);
90
- }
91
-
92
- function textResponse(text: string): ProviderResponse {
93
- return response([{ type: "text", text }]);
94
- }
95
-
96
- let workspaceDir: string;
97
-
98
- beforeEach(async () => {
99
- workspaceDir = await mkdtemp(join(tmpdir(), "v3-assign-"));
100
- });
101
-
102
- afterEach(async () => {
103
- await rm(workspaceDir, { recursive: true, force: true });
104
- });
105
-
106
- async function makePage(
107
- slug: string,
108
- frontmatter: Partial<ConceptPageFrontmatter>,
109
- body = "page body",
110
- ): Promise<void> {
111
- await writePage(workspaceDir, {
112
- slug,
113
- frontmatter: { edges: [], ref_files: [], ref_urls: [], ...frontmatter },
114
- body,
115
- });
116
- }
117
-
118
- describe("assignPages", () => {
119
- test("unions classified picks with prior picks, never overwriting", async () => {
120
- await makePage("page-a", { leaves: ["domain-a/topic-x"] });
121
- // Classifier returns [2] -> domain-a/topic-y and [1] -> already present.
122
- const provider = stubProvider(() => toolResponse([2, 1]));
123
-
124
- const results = await assignPages({
125
- tree: threeLeafTree(),
126
- workspaceDir,
127
- slugs: ["page-a"],
128
- provider,
129
- throttleMs: 0,
130
- });
131
-
132
- expect(results).toEqual([
133
- {
134
- slug: "page-a",
135
- before: ["domain-a/topic-x"],
136
- after: ["domain-a/topic-x", "domain-a/topic-y"],
137
- failed: false,
138
- },
139
- ]);
140
-
141
- const written = await readPage(workspaceDir, "page-a");
142
- expect(written?.frontmatter.leaves).toEqual([
143
- "domain-a/topic-x",
144
- "domain-a/topic-y",
145
- ]);
146
- });
147
-
148
- test("classification failure leaves the page's leaves unchanged", async () => {
149
- await makePage("page-a", { leaves: ["domain-b/topic-z"] });
150
-
151
- // (a) provider throws
152
- const throwing = stubProvider(() => {
153
- throw new Error("boom");
154
- });
155
- let results = await assignPages({
156
- tree: threeLeafTree(),
157
- workspaceDir,
158
- slugs: ["page-a"],
159
- provider: throwing,
160
- throttleMs: 0,
161
- });
162
- expect(results[0]).toEqual({
163
- slug: "page-a",
164
- before: ["domain-b/topic-z"],
165
- after: ["domain-b/topic-z"],
166
- failed: true,
167
- });
168
- expect(
169
- (await readPage(workspaceDir, "page-a"))?.frontmatter.leaves,
170
- ).toEqual(["domain-b/topic-z"]);
171
-
172
- // (b) no tool use
173
- results = await assignPages({
174
- tree: threeLeafTree(),
175
- workspaceDir,
176
- slugs: ["page-a"],
177
- provider: stubProvider(() => textResponse("no tool here")),
178
- throttleMs: 0,
179
- });
180
- expect(results[0].failed).toBe(true);
181
- expect(
182
- (await readPage(workspaceDir, "page-a"))?.frontmatter.leaves,
183
- ).toEqual(["domain-b/topic-z"]);
184
-
185
- // (c) empty ids
186
- results = await assignPages({
187
- tree: threeLeafTree(),
188
- workspaceDir,
189
- slugs: ["page-a"],
190
- provider: stubProvider(() => toolResponse([])),
191
- throttleMs: 0,
192
- });
193
- expect(results[0].failed).toBe(true);
194
- expect(
195
- (await readPage(workspaceDir, "page-a"))?.frontmatter.leaves,
196
- ).toEqual(["domain-b/topic-z"]);
197
- });
198
-
199
- test("deterministic id->path mapping, dropping out-of-range and 0 ids", async () => {
200
- await makePage("page-a", {});
201
- // 0 and 99 are invalid; 3 -> domain-b/topic-z; 1 -> domain-a/topic-x.
202
- const provider = stubProvider(() => toolResponse([0, 99, 3, 1, -1, 2.5]));
203
-
204
- const results = await assignPages({
205
- tree: threeLeafTree(),
206
- workspaceDir,
207
- slugs: ["page-a"],
208
- provider,
209
- throttleMs: 0,
210
- });
211
-
212
- expect(results[0].after).toEqual(["domain-b/topic-z", "domain-a/topic-x"]);
213
- expect(results[0].failed).toBe(false);
214
- });
215
-
216
- test("defaults to pages with empty or missing leaves", async () => {
217
- await makePage("page-empty", { leaves: [] });
218
- await makePage("page-missing", {});
219
- await makePage("page-classified", { leaves: ["domain-a/topic-x"] });
220
-
221
- const seen: string[] = [];
222
- const provider = stubProvider((messages) => {
223
- const text = messages[0]?.content
224
- .map((b) => (b.type === "text" ? b.text : ""))
225
- .join("");
226
- seen.push(text);
227
- return toolResponse([2]);
228
- });
229
-
230
- const results = await assignPages({
231
- tree: threeLeafTree(),
232
- workspaceDir,
233
- provider,
234
- throttleMs: 0,
235
- });
236
-
237
- const slugs = results.map((r) => r.slug).sort();
238
- expect(slugs).toEqual(["page-empty", "page-missing"]);
239
- // The classified page was never sent to the provider.
240
- expect(seen.length).toBe(2);
241
- });
242
- });
@@ -1,39 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, describe, expect, test } from "bun:test";
5
-
6
- import { loadCore } from "../core.js";
7
-
8
- const BUNDLED_DATA_DIR = join(import.meta.dir, "..", "data");
9
-
10
- describe("loadCore", () => {
11
- let tmpDirs: string[] = [];
12
-
13
- afterEach(async () => {
14
- await Promise.all(
15
- tmpDirs.map((d) => rm(d, { recursive: true, force: true })),
16
- );
17
- tmpDirs = [];
18
- });
19
-
20
- async function makeTmpDir(): Promise<string> {
21
- const dir = await mkdtemp(join(tmpdir(), "v3-core-"));
22
- tmpDirs.push(dir);
23
- return dir;
24
- }
25
-
26
- test("loads alwaysOn from the supplied dataDir's core.json into a Set", async () => {
27
- const core = await loadCore(BUNDLED_DATA_DIR);
28
- expect(core).toBeInstanceOf(Set);
29
- expect(core.has("domain-a/topic-x")).toBe(true);
30
- expect(core.size).toBe(1);
31
- });
32
-
33
- test("returns empty set when core.json is missing", async () => {
34
- const emptyDataDir = await makeTmpDir();
35
- const core = await loadCore(emptyDataDir);
36
- expect(core).toBeInstanceOf(Set);
37
- expect(core.size).toBe(0);
38
- });
39
- });