@vellumai/assistant 0.8.10-dev.202606092238.d04fd59 → 0.8.10

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 (143) hide show
  1. package/AGENTS.md +1 -1
  2. package/examples/plugins/echo/README.md +3 -3
  3. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +0 -2
  4. package/openapi.yaml +11 -37
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +14 -19
  7. package/src/__tests__/build-persisted-content.test.ts +1 -75
  8. package/src/__tests__/config-managed-gemini-defaults.test.ts +46 -2
  9. package/src/__tests__/context-overflow-reducer.test.ts +124 -264
  10. package/src/__tests__/conversation-agent-loop-overflow.test.ts +22 -22
  11. package/src/__tests__/conversation-agent-loop.test.ts +37 -45
  12. package/src/__tests__/conversation-fork-crud.test.ts +0 -110
  13. package/src/__tests__/conversation-provider-retry-repair.test.ts +20 -16
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +5 -118
  15. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -4
  16. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +142 -91
  17. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -3
  18. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  19. package/src/__tests__/embedding-managed-proxy-selection.test.ts +40 -6
  20. package/src/__tests__/external-plugin-loader.test.ts +1 -0
  21. package/src/__tests__/guardian-routing-invariants.test.ts +0 -91
  22. package/src/__tests__/history-repair-hook.test.ts +0 -2
  23. package/src/__tests__/host-app-control-proxy.test.ts +0 -45
  24. package/src/__tests__/memory-retrieval-hook.test.ts +15 -29
  25. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  26. package/src/__tests__/persistence-secret-redaction.test.ts +0 -78
  27. package/src/__tests__/plugin-bootstrap.test.ts +73 -82
  28. package/src/__tests__/plugin-skill-contribution.test.ts +355 -0
  29. package/src/__tests__/plugin-tool-contribution.test.ts +4 -7
  30. package/src/__tests__/plugin-types.test.ts +8 -0
  31. package/src/__tests__/provider-catalog-visibility.test.ts +9 -1
  32. package/src/__tests__/server-history-render.test.ts +0 -39
  33. package/src/__tests__/skills.test.ts +0 -123
  34. package/src/__tests__/title-generate-hook.test.ts +0 -2
  35. package/src/__tests__/tool-audit-listener.test.ts +0 -51
  36. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -32
  37. package/src/agent/loop.ts +23 -26
  38. package/src/api/events/tool-result.ts +0 -9
  39. package/src/api/events/tool-use-start.ts +0 -7
  40. package/src/api/responses/conversation-message.ts +14 -91
  41. package/src/approvals/guardian-request-resolvers.ts +0 -26
  42. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  43. package/src/cli/commands/inference-providers.ts +8 -0
  44. package/src/cli/commands/plugins.ts +2 -2
  45. package/src/cli/lib/__tests__/install-from-github.test.ts +18 -174
  46. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -1
  47. package/src/cli/lib/__tests__/plugin-details.test.ts +1 -1
  48. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +1 -1
  49. package/src/cli/lib/__tests__/search-plugins.test.ts +11 -11
  50. package/src/cli/lib/install-from-github.ts +19 -130
  51. package/src/cli/lib/plugin-catalog-cache.ts +1 -1
  52. package/src/cli/lib/plugin-details.ts +4 -4
  53. package/src/cli/lib/plugin-marketplace.ts +2 -2
  54. package/src/cli/lib/search-plugins.ts +6 -6
  55. package/src/config/bundled-skills/skill-management/SKILL.md +1 -89
  56. package/src/config/env-registry.ts +0 -12
  57. package/src/config/feature-flag-registry.json +80 -0
  58. package/src/config/loader.ts +22 -5
  59. package/src/config/seed-inference-profiles.ts +1 -1
  60. package/src/config/skills.ts +47 -101
  61. package/src/context/strip-injections.ts +4 -7
  62. package/src/daemon/conversation-agent-loop-handlers.ts +6 -102
  63. package/src/daemon/conversation-agent-loop.ts +232 -134
  64. package/src/daemon/conversation-runtime-assembly.ts +8 -44
  65. package/src/daemon/conversation-tool-setup.ts +1 -9
  66. package/src/daemon/external-plugins-bootstrap.ts +87 -33
  67. package/src/daemon/handlers/config-channels.ts +2 -11
  68. package/src/daemon/handlers/shared.ts +1 -9
  69. package/src/daemon/handlers/skills.ts +2 -6
  70. package/src/daemon/host-app-control-proxy.ts +57 -72
  71. package/src/daemon/message-protocol.ts +7 -0
  72. package/src/daemon/message-types/browser.ts +10 -0
  73. package/src/daemon/message-types/skills.ts +0 -4
  74. package/src/events/tool-audit-listener.ts +1 -7
  75. package/src/memory/conversation-crud.ts +1 -44
  76. package/src/memory/db-init.ts +0 -4
  77. package/src/memory/embedding-backend.ts +22 -5
  78. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  79. package/src/memory/migrations/index.ts +0 -2
  80. package/src/memory/schema/conversations.ts +0 -3
  81. package/src/memory/tool-usage-store.ts +3 -4
  82. package/src/memory/v2/activation-store.ts +1 -50
  83. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +187 -0
  84. package/src/notifications/adapters/slack.ts +3 -3
  85. package/src/notifications/adapters/telegram.ts +1 -2
  86. package/src/notifications/copy-composer.ts +267 -49
  87. package/src/notifications/decision-engine.ts +35 -16
  88. package/src/notifications/home-feed-side-effect.ts +6 -1
  89. package/src/platform/client.ts +3 -1
  90. package/src/platform/feature-gate.ts +12 -3
  91. package/src/plugin-api/constants.ts +0 -2
  92. package/src/plugin-api/index.ts +6 -34
  93. package/src/plugin-api/types.ts +0 -19
  94. package/src/plugins/defaults/compaction/compact.ts +1 -39
  95. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +32 -240
  96. package/src/plugins/defaults/compaction/window-manager.ts +0 -136
  97. package/src/plugins/defaults/index.ts +0 -24
  98. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +43 -68
  99. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit.ts → user-prompt-submit-temp.ts} +67 -36
  100. package/src/plugins/external-api.ts +1 -1
  101. package/src/plugins/plugin-skill-contributions.ts +292 -0
  102. package/src/plugins/types.ts +51 -5
  103. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +2 -1
  104. package/src/providers/model-catalog.ts +1 -0
  105. package/src/providers/registry.ts +8 -0
  106. package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -151
  107. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +62 -0
  108. package/src/runtime/routes/__tests__/plugins-routes.test.ts +4 -4
  109. package/src/runtime/routes/acp-routes.test.ts +0 -106
  110. package/src/runtime/routes/acp-routes.ts +2 -248
  111. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  112. package/src/runtime/routes/channel-verification-routes.ts +5 -14
  113. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +14 -0
  114. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +34 -33
  115. package/src/runtime/routes/inference-provider-connection-routes.ts +45 -0
  116. package/src/runtime/routes/plugins-routes.ts +3 -3
  117. package/src/runtime/verification-outbound-actions.ts +2 -147
  118. package/src/runtime/verification-templates.ts +3 -29
  119. package/src/telemetry/types.ts +1 -25
  120. package/src/telemetry/usage-telemetry-reporter.test.ts +7 -141
  121. package/src/telemetry/usage-telemetry-reporter.ts +7 -63
  122. package/src/tools/executor.ts +1 -4
  123. package/src/tools/skills/load.ts +6 -11
  124. package/src/tools/terminal/safe-env.ts +0 -1
  125. package/src/tools/types.ts +0 -10
  126. package/src/__tests__/context-window-manager-overflow-rung.test.ts +0 -289
  127. package/src/__tests__/conversation-tool-setup-skill-attribution.test.ts +0 -116
  128. package/src/__tests__/conversation-tool-setup-test-helpers.ts +0 -98
  129. package/src/__tests__/corrected-target.test.ts +0 -93
  130. package/src/__tests__/tool-start-timestamp.test.ts +0 -218
  131. package/src/memory/__tests__/tool-invocation-test-helpers.ts +0 -69
  132. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +0 -81
  133. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +0 -20
  134. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +0 -68
  135. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +0 -20
  136. package/src/memory/tool-execution-events-store.test.ts +0 -162
  137. package/src/memory/tool-execution-events-store.ts +0 -73
  138. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +0 -51
  139. package/src/memory/v2/injected-block-slugs.ts +0 -30
  140. package/src/notifications/__tests__/copy-composer.test.ts +0 -244
  141. package/src/notifications/access-request-copy.ts +0 -298
  142. package/src/plugins/defaults/compaction/corrected-target.ts +0 -53
  143. package/src/plugins/defaults/memory-retrieval/package.json +0 -14
package/AGENTS.md CHANGED
@@ -84,7 +84,7 @@ The rule exists because test machinery and production code have **inverted invar
84
84
 
85
85
  Concretely:
86
86
 
87
- - **Test helpers** (e.g. `src/__tests__/*-test-helpers.ts`) use only node stdlib, `bun:test`, and sibling helpers. Type-only imports (`import type`) of production types are permitted: they are erased at compile time and carry no import-time side effects, so the isolation invariant holds — prefer them over duplicating non-trivial production types structurally. If helpers need to manipulate shared state that production code also reads, both sides declare a typed slot under `globalThis.vellumAssistant.*` and read/write that slot independently. The slot shape is duplicated by design — the helper and the production module both reference the namespace, neither imports the other.
87
+ - **Test helpers** (e.g. `src/__tests__/*-test-helpers.ts`) use only node stdlib, `bun:test`, and sibling helpers. If they need to manipulate shared state that production code also reads, both sides declare a typed slot under `globalThis.vellumAssistant.*` and read/write that slot independently. The slot shape is duplicated by design — the helper and the production module both reference the namespace, neither imports the other.
88
88
  - **The test preload** (`src/__tests__/test-preload.ts`) is the strictest: it must not import from `src/` at all. Its only static imports are node stdlib, `bun:test`, and helpers in `src/__tests__/`. Importing from a source module risks running its import-time side effects before the workspace override is set.
89
89
  - **The preload verifier** (`src/__tests__/test-preload-verifier.ts`) runs after the main preload and asserts the override took effect (`VELLUM_WORKSPACE_DIR` must resolve under `os.tmpdir()`).
90
90
  - **Destructive ops** (e.g. `rmSync(dbPath, ...)`) in tests must call `assertNotLiveDb(path)` from `src/__tests__/assert-not-live-db.js` immediately before the destructive call. The check is a per-callsite belt to the preload-verifier suspenders.
@@ -16,8 +16,8 @@ to eyeball which hooks fire during a conversation.
16
16
 
17
17
  For the full plugin authoring guide — manifest shape, every contribution
18
18
  surface, hook patterns, and conventions — see
19
- [`plugins/README.md`](../../../../plugins/README.md).
20
- [`simple-memory`](../../../../plugins/simple-memory/) is the
19
+ [`experimental/plugins/README.md`](../../../../experimental/plugins/README.md).
20
+ [`simple-memory`](../../../../experimental/plugins/simple-memory/) is the
21
21
  canonical reference implementation that exercises every wired surface.
22
22
 
23
23
  ## What it does
@@ -115,7 +115,7 @@ vellum restart
115
115
 
116
116
  ## Next steps
117
117
 
118
- - Read [`plugins/README.md`](../../../../plugins/README.md)
118
+ - Read [`experimental/plugins/README.md`](../../../../experimental/plugins/README.md)
119
119
  for the full plugin authoring guide: manifest shape, every contribution
120
120
  surface, hook patterns (observe / transform), tool contributions, and
121
121
  conventions.
@@ -195,8 +195,6 @@ interface ToolLifecycleEventBase {
195
195
  conversationId: string;
196
196
  requestId?: string;
197
197
  executionTarget?: ExecutionTarget;
198
- /** Id of the skill whose `skill_execute` dispatch triggered this tool call. Absent for direct tool calls. */
199
- skillId?: string;
200
198
  }
201
199
 
202
200
  export interface AllowlistOption {
package/openapi.yaml CHANGED
@@ -96,11 +96,8 @@ paths:
96
96
  operationId: acp_by_id_steer_post
97
97
  summary: Steer ACP session
98
98
  description:
99
- "Send a steering instruction to an ACP session. Sessions no longer in memory (completed, or lost to a
100
- daemon restart) are transparently resumed from persisted history first, when the agent supports ACP session
101
- loading. Resuming a terminal session re-spawns the host agent, so it requires guardian approval: the route acks
102
- immediately with approvalPending=true and performs the resume in the background once approved, streaming results
103
- over SSE."
99
+ Send a steering instruction to an ACP session. Sessions no longer in memory (completed, or lost to a daemon
100
+ restart) are transparently resumed from persisted history first, when the agent supports ACP session loading.
104
101
  tags:
105
102
  - acp
106
103
  responses:
@@ -118,11 +115,6 @@ paths:
118
115
  resumed:
119
116
  description: True when the session was resumed from persisted history before steering.
120
117
  type: boolean
121
- approvalPending:
122
- description:
123
- True when the steer triggered a guardian-approval-gated resume that will run asynchronously; watch SSE for
124
- the outcome.
125
- type: boolean
126
118
  required:
127
119
  - acpSessionId
128
120
  - steered
@@ -15945,8 +15937,6 @@ paths:
15945
15937
  - assistant
15946
15938
  content:
15947
15939
  type: string
15948
- deprecated: true
15949
- description: "Deprecated: superseded by contentBlocks. Flat plain-text body (joined text segments)."
15950
15940
  timestamp:
15951
15941
  type: string
15952
15942
  attachments:
@@ -16251,8 +16241,6 @@ paths:
16251
16241
  - name
16252
16242
  - input
16253
16243
  additionalProperties: false
16254
- deprecated: true
16255
- description: "Deprecated: superseded by contentBlocks (the tool_use variant). Flat list of tool calls."
16256
16244
  surfaces:
16257
16245
  type: array
16258
16246
  items:
@@ -16304,28 +16292,18 @@ paths:
16304
16292
  - surfaceType
16305
16293
  - data
16306
16294
  additionalProperties: false
16307
- deprecated: true
16308
- description: "Deprecated: superseded by contentBlocks (the surface variant). Flat list of surfaces."
16309
16295
  textSegments:
16310
16296
  type: array
16311
16297
  items:
16312
16298
  type: string
16313
- deprecated: true
16314
- description: "Deprecated: superseded by contentBlocks. Text segments split by tool-call boundaries."
16315
16299
  thinkingSegments:
16316
16300
  type: array
16317
16301
  items:
16318
16302
  type: string
16319
- deprecated: true
16320
- description: "Deprecated: superseded by contentBlocks. Reasoning text extracted from thinking blocks."
16321
16303
  contentOrder:
16322
16304
  type: array
16323
16305
  items:
16324
16306
  type: string
16325
- deprecated: true
16326
- description:
16327
- 'Deprecated: superseded by contentBlocks. Positional "<type>:<index>" content ordering (e.g. "text:0",
16328
- "thinking:1").'
16329
16307
  contentBlocks:
16330
16308
  type: array
16331
16309
  items:
@@ -16348,10 +16326,6 @@ paths:
16348
16326
  const: thinking
16349
16327
  thinking:
16350
16328
  type: string
16351
- startedAt:
16352
- type: number
16353
- completedAt:
16354
- type: number
16355
16329
  required:
16356
16330
  - type
16357
16331
  - thinking
@@ -19475,11 +19449,11 @@ paths:
19475
19449
  operationId: plugins_install_post
19476
19450
  summary: Install a plugin
19477
19451
  description:
19478
- "Install a plugin by name from the canonical source — a whitelisted `plugins/marketplace.json` entry, else
19479
- the first-party `plugins/<name>/` convention. Always resolves against the curated default git ref (no
19480
- caller-supplied ref): installing from an unreviewed revision would bypass the marketplace/first-party curation
19481
- boundary and let attacker-controlled code be loaded. Materializes the plugin under
19482
- `<workspaceDir>/plugins/<name>/`; the assistant must be restarted to load it. Mirrors the CLI's `assistant
19452
+ "Install a plugin by name from the canonical source — a whitelisted `experimental/plugins/marketplace.json`
19453
+ entry, else the first-party `experimental/plugins/<name>/` convention. Always resolves against the curated
19454
+ default git ref (no caller-supplied ref): installing from an unreviewed revision would bypass the
19455
+ marketplace/first-party curation boundary and let attacker-controlled code be loaded. Materializes the plugin
19456
+ under `<workspaceDir>/plugins/<name>/`; the assistant must be restarted to load it. Mirrors the CLI's `assistant
19483
19457
  plugins install <name>`. An already-installed name without `force` returns 409; a name that resolves to nothing
19484
19458
  returns 404. Sibling to `POST /v1/skills/install`."
19485
19459
  tags:
@@ -19541,9 +19515,9 @@ paths:
19541
19515
  operationId: plugins_search_get
19542
19516
  summary: Search the plugin catalog
19543
19517
  description:
19544
- List installable plugins from the canonical `vellum-ai/vellum-assistant` catalog at `plugins/`. The query
19545
- is an ECMAScript regex matched case-insensitively against the directory name (e.g. `memory`, `^simple`). Empty
19546
- query returns every entry. Mirrors the CLI's `assistant plugins search`.
19518
+ List installable plugins from the canonical `vellum-ai/vellum-assistant` catalog at
19519
+ `experimental/plugins/`. The query is an ECMAScript regex matched case-insensitively against the directory name
19520
+ (e.g. `memory`, `^simple`). Empty query returns every entry. Mirrors the CLI's `assistant plugins search`.
19547
19521
  tags:
19548
19522
  - plugins
19549
19523
  responses:
@@ -19571,7 +19545,7 @@ paths:
19571
19545
  path:
19572
19546
  type: string
19573
19547
  description:
19574
- "Human-readable origin: a repo-relative path (`plugins/<name>`) for first-party plugins or a
19548
+ "Human-readable origin: a repo-relative path (`experimental/plugins/<name>`) for first-party plugins or a
19575
19549
  `github:owner/repo@ref` locator for external ones."
19576
19550
  description:
19577
19551
  description: Short description, when known (external entries only today).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.10-dev.202606092238.d04fd59",
3
+ "version": "0.8.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,7 +13,7 @@
13
13
  * Sites not exercised here (`aborted_via_error`) require deeper provider
14
14
  * fakery and are best covered by integration tests.
15
15
  */
16
- import { afterEach, describe, expect, test } from "bun:test";
16
+ import { afterEach, describe, expect, mock, test } from "bun:test";
17
17
 
18
18
  import type {
19
19
  AgentEvent,
@@ -23,14 +23,12 @@ import type {
23
23
  import { AgentLoop, isMaxTokensStopReason } from "../agent/loop.js";
24
24
  import type { ContextWindowConfig } from "../config/types.js";
25
25
  import type { TrustContext } from "../daemon/trust-context.js";
26
- import { HOOKS } from "../plugin-api/constants.js";
27
26
  import {
28
27
  createContextWindowManager,
29
28
  disposeContextWindowManager,
30
29
  getContextWindowManager,
31
30
  } from "../plugins/defaults/compaction/manager-store.js";
32
31
  import type { PostCompactContext } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
33
- import { registerPlugin } from "../plugins/registry.js";
34
32
  import type {
35
33
  Message,
36
34
  Provider,
@@ -39,25 +37,22 @@ import type {
39
37
  ToolDefinition,
40
38
  } from "../providers/types.js";
41
39
 
42
- // The agent loop runs the default post-compaction re-injection through the
43
- // `post-compact` hook chain when it compacts in place. Register a test hook on
44
- // that chain so these unit tests can drive the re-injection result without the
45
- // daemon-level injector. The hook writes the re-injected history back onto the
46
- // context; tests assign `postCompactImpl` to observe the call or force a
47
- // failure; when unset the hook leaves the history it was handed untouched.
40
+ // The agent loop invokes the default post-compaction re-injection hook directly
41
+ // when it compacts in place. Stub it so these unit tests can drive the
42
+ // re-injection result without the daemon-level injector chain. Tests assign
43
+ // `postCompactImpl` to observe the call or force a failure; when unset
44
+ // the hook is a no-op that returns the history it was handed.
48
45
  let postCompactImpl:
49
46
  | ((input: PostCompactContext) => Promise<Message[]>)
50
47
  | null = null;
51
- registerPlugin({
52
- manifest: { name: "test-post-compact", version: "0.0.0" },
53
- hooks: {
54
- [HOOKS.POST_COMPACT]: async (input: PostCompactContext): Promise<void> => {
55
- input.history = postCompactImpl
56
- ? await postCompactImpl(input)
57
- : input.history;
58
- },
59
- },
60
- });
48
+ mock.module(
49
+ "../plugins/defaults/memory-retrieval/hooks/post-compact.js",
50
+ () => ({
51
+ default: async (input: PostCompactContext) => ({
52
+ messages: postCompactImpl ? await postCompactImpl(input) : input.history,
53
+ }),
54
+ }),
55
+ );
61
56
 
62
57
  // ---------------------------------------------------------------------------
63
58
  // Helpers (mirrored from agent-loop.test.ts so this file is self-contained)
@@ -54,10 +54,7 @@ mock.module("../memory/llm-request-log-store.js", () => ({
54
54
  }));
55
55
 
56
56
  // ── Imports (after mocks) ─────────────────────────────────────────────────────
57
- import {
58
- buildPersistedAssistantContent,
59
- stampThinkingTiming,
60
- } from "../daemon/conversation-agent-loop-handlers.js";
57
+ import { buildPersistedAssistantContent } from "../daemon/conversation-agent-loop-handlers.js";
61
58
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
62
59
  import type { ContentBlock } from "../providers/types.js";
63
60
 
@@ -185,74 +182,3 @@ describe("buildPersistedAssistantContent — native activityMetadata", () => {
185
182
  expect(block._activityMetadata).toBeUndefined();
186
183
  });
187
184
  });
188
-
189
- describe("stampThinkingTiming", () => {
190
- test("stamps internal timing onto thinking blocks by position", () => {
191
- // GIVEN a turn that interleaves text and two thinking blocks AND the
192
- // per-block timing captured while streaming (one entry per thinking block,
193
- // in stream order)
194
- const content = [
195
- { type: "thinking", thinking: "first", signature: "s1" },
196
- { type: "text", text: "answer" },
197
- { type: "thinking", thinking: "second", signature: "s2" },
198
- ] as unknown as ContentBlock[];
199
- const timings = [
200
- { startedAt: 100, completedAt: 250 },
201
- { startedAt: 400, completedAt: 480 },
202
- ];
203
-
204
- // WHEN the content is stamped before persistence
205
- const stamped = stampThinkingTiming(content, timings) as unknown as Array<
206
- Record<string, unknown>
207
- >;
208
-
209
- // THEN each thinking block carries the `_`-prefixed timing for its position
210
- expect(stamped[0]).toMatchObject({
211
- type: "thinking",
212
- _startedAt: 100,
213
- _completedAt: 250,
214
- });
215
- expect(stamped[2]).toMatchObject({
216
- type: "thinking",
217
- _startedAt: 400,
218
- _completedAt: 480,
219
- });
220
- // AND the interleaved text block is left untouched
221
- expect(stamped[1]).toEqual({ type: "text", text: "answer" });
222
- });
223
-
224
- test("leaves thinking blocks unstamped when no timing was captured", () => {
225
- // GIVEN thinking content but an empty timing list (thinking streaming was
226
- // disabled, so no per-block timing was recorded this turn)
227
- const content = [
228
- { type: "thinking", thinking: "first", signature: "s1" },
229
- ] as unknown as ContentBlock[];
230
-
231
- // WHEN the content is stamped with no timing
232
- const stamped = stampThinkingTiming(content, []);
233
-
234
- // THEN the original content is returned unchanged so the UI hides duration,
235
- // exactly as a tool call with no timing
236
- expect(stamped).toBe(content);
237
- expect(stamped[0]).not.toHaveProperty("_startedAt");
238
- });
239
-
240
- test("stamps only the thinking blocks that have a matching timing entry", () => {
241
- // GIVEN two thinking blocks but only one captured timing entry (e.g. the
242
- // second block opened after the timing array was already finalized)
243
- const content = [
244
- { type: "thinking", thinking: "first", signature: "s1" },
245
- { type: "thinking", thinking: "second", signature: "s2" },
246
- ] as unknown as ContentBlock[];
247
- const timings = [{ startedAt: 100, completedAt: 250 }];
248
-
249
- // WHEN the content is stamped
250
- const stamped = stampThinkingTiming(content, timings) as unknown as Array<
251
- Record<string, unknown>
252
- >;
253
-
254
- // THEN the first block is stamped and the unmatched second block is left as-is
255
- expect(stamped[0]).toMatchObject({ _startedAt: 100, _completedAt: 250 });
256
- expect(stamped[1]).not.toHaveProperty("_startedAt");
257
- });
258
- });
@@ -57,8 +57,17 @@ mock.module("../util/logger.js", () => ({
57
57
  getLogger: () => makeLoggerStub(),
58
58
  }));
59
59
 
60
+ // ---------------------------------------------------------------------------
61
+ // Feature flag mock — controls whether managed-gemini-embeddings-enabled is on
62
+ // ---------------------------------------------------------------------------
63
+
64
+ let featureFlagEnabled = false;
65
+
60
66
  mock.module("../config/assistant-feature-flags.js", () => ({
61
- isAssistantFeatureFlagEnabled: () => true,
67
+ isAssistantFeatureFlagEnabled: (key: string) => {
68
+ if (key === "managed-gemini-embeddings-enabled") return featureFlagEnabled;
69
+ return true;
70
+ },
62
71
  clearFeatureFlagOverridesCache: () => {},
63
72
  initFeatureFlagOverrides: async () => {},
64
73
  getAssistantFeatureFlagDefaults: () => ({}),
@@ -110,6 +119,8 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
110
119
  setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
111
120
  invalidateConfigCache();
112
121
 
122
+ // Reset mock state
123
+ featureFlagEnabled = false;
113
124
  originalIsPlatform = process.env.IS_PLATFORM;
114
125
  delete process.env.IS_PLATFORM;
115
126
  });
@@ -126,9 +137,10 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
126
137
  }
127
138
  });
128
139
 
129
- test("applies managed Gemini defaults when IS_PLATFORM + provider auto", () => {
140
+ test("applies managed Gemini defaults when FF on + IS_PLATFORM + provider auto", () => {
130
141
  writeConfig({});
131
142
 
143
+ featureFlagEnabled = true;
132
144
  process.env.IS_PLATFORM = "true";
133
145
 
134
146
  const config = loadConfig();
@@ -150,9 +162,22 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
150
162
  expect(qdrantRaw.vectorSize).toBe(3072);
151
163
  });
152
164
 
165
+ test("does NOT apply when feature flag is OFF", () => {
166
+ writeConfig({});
167
+
168
+ featureFlagEnabled = false;
169
+ process.env.IS_PLATFORM = "true";
170
+
171
+ const config = loadConfig();
172
+
173
+ expect(config.memory.embeddings.provider).toBe("auto");
174
+ expect(config.memory.qdrant.vectorSize).toBe(384);
175
+ });
176
+
153
177
  test("does NOT apply when IS_PLATFORM is not set", () => {
154
178
  writeConfig({});
155
179
 
180
+ featureFlagEnabled = true;
156
181
  delete process.env.IS_PLATFORM;
157
182
 
158
183
  const config = loadConfig();
@@ -166,6 +191,7 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
166
191
  memory: { embeddings: { provider: "local" } },
167
192
  });
168
193
 
194
+ featureFlagEnabled = true;
169
195
  process.env.IS_PLATFORM = "true";
170
196
 
171
197
  const config = loadConfig();
@@ -178,6 +204,7 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
178
204
  memory: { embeddings: { provider: "openai" } },
179
205
  });
180
206
 
207
+ featureFlagEnabled = true;
181
208
  process.env.IS_PLATFORM = "true";
182
209
 
183
210
  const config = loadConfig();
@@ -192,6 +219,7 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
192
219
  },
193
220
  });
194
221
 
222
+ featureFlagEnabled = true;
195
223
  process.env.IS_PLATFORM = "true";
196
224
 
197
225
  const config = loadConfig();
@@ -207,6 +235,7 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
207
235
  memory: { embeddings: { provider: "ollama" } },
208
236
  });
209
237
 
238
+ featureFlagEnabled = true;
210
239
  process.env.IS_PLATFORM = "true";
211
240
 
212
241
  const config = loadConfig();
@@ -216,6 +245,7 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
216
245
  test("is idempotent — second loadConfig is a no-op after migration", () => {
217
246
  writeConfig({});
218
247
 
248
+ featureFlagEnabled = true;
219
249
  process.env.IS_PLATFORM = "true";
220
250
 
221
251
  const config = loadConfig();
@@ -244,6 +274,7 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
244
274
  },
245
275
  });
246
276
 
277
+ featureFlagEnabled = true;
247
278
  process.env.IS_PLATFORM = "true";
248
279
 
249
280
  const config = loadConfig();
@@ -264,9 +295,22 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
264
295
  expect(qdrantRaw.onDisk).toBe(false);
265
296
  });
266
297
 
298
+ test("does NOT apply when both FF off and IS_PLATFORM not set", () => {
299
+ writeConfig({});
300
+
301
+ featureFlagEnabled = false;
302
+ delete process.env.IS_PLATFORM;
303
+
304
+ const config = loadConfig();
305
+
306
+ expect(config.memory.embeddings.provider).toBe("auto");
307
+ expect(config.memory.qdrant.vectorSize).toBe(384);
308
+ });
309
+
267
310
  test("applies when IS_PLATFORM is '1'", () => {
268
311
  writeConfig({});
269
312
 
313
+ featureFlagEnabled = true;
270
314
  process.env.IS_PLATFORM = "1";
271
315
 
272
316
  const config = loadConfig();