@vellumai/assistant 0.8.9 → 0.8.10-dev.202606092334.09948c8

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 (161) 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 +2 -0
  4. package/openapi.yaml +38 -12
  5. package/package.json +1 -1
  6. package/scripts/sync-llm-catalog.ts +2 -0
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +21 -15
  8. package/src/__tests__/approval-cascade.test.ts +1 -0
  9. package/src/__tests__/build-persisted-content.test.ts +75 -1
  10. package/src/__tests__/compaction-events.test.ts +1 -0
  11. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  12. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  13. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  14. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  15. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +11 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +90 -49
  17. package/src/__tests__/conversation-agent-loop.test.ts +113 -64
  18. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  19. package/src/__tests__/conversation-fork-crud.test.ts +110 -0
  20. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  21. package/src/__tests__/conversation-provider-retry-repair.test.ts +48 -48
  22. package/src/__tests__/conversation-queue.test.ts +1 -0
  23. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  24. package/src/__tests__/conversation-slash-queue.test.ts +1 -0
  25. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  26. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +91 -142
  29. package/src/__tests__/conversation-tool-setup-skill-attribution.test.ts +116 -0
  30. package/src/__tests__/conversation-tool-setup-test-helpers.ts +98 -0
  31. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  32. package/src/__tests__/conversation-workspace-injection.test.ts +1 -0
  33. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  34. package/src/__tests__/corrected-target.test.ts +93 -0
  35. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  36. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  37. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  38. package/src/__tests__/external-plugin-loader.test.ts +0 -1
  39. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  40. package/src/__tests__/history-repair-hook.test.ts +2 -0
  41. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  42. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  43. package/src/__tests__/memory-retrieval-hook.test.ts +29 -15
  44. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  45. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  46. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  47. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  48. package/src/__tests__/plugin-types.test.ts +0 -8
  49. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  50. package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
  51. package/src/__tests__/server-history-render.test.ts +39 -0
  52. package/src/__tests__/skills.test.ts +123 -0
  53. package/src/__tests__/title-generate-hook.test.ts +2 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +51 -0
  55. package/src/__tests__/tool-executor-lifecycle-events.test.ts +32 -0
  56. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  57. package/src/agent/loop.ts +26 -39
  58. package/src/api/events/tool-result.ts +9 -0
  59. package/src/api/events/tool-use-start.ts +7 -0
  60. package/src/api/responses/conversation-message.ts +91 -14
  61. package/src/approvals/guardian-request-resolvers.ts +26 -0
  62. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  63. package/src/cli/commands/inference-providers.ts +0 -8
  64. package/src/cli/commands/plugins.ts +2 -2
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +174 -18
  66. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -1
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +1 -1
  68. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +1 -1
  69. package/src/cli/lib/__tests__/search-plugins.test.ts +11 -11
  70. package/src/cli/lib/install-from-github.ts +130 -19
  71. package/src/cli/lib/plugin-catalog-cache.ts +1 -1
  72. package/src/cli/lib/plugin-details.ts +4 -4
  73. package/src/cli/lib/plugin-marketplace.ts +2 -2
  74. package/src/cli/lib/search-plugins.ts +6 -6
  75. package/src/config/bundled-skills/skill-management/SKILL.md +89 -1
  76. package/src/config/env-registry.ts +12 -0
  77. package/src/config/feature-flag-registry.json +0 -80
  78. package/src/config/loader.ts +5 -22
  79. package/src/config/seed-inference-profiles.ts +1 -1
  80. package/src/config/skills.ts +101 -47
  81. package/src/context/strip-injections.ts +7 -4
  82. package/src/daemon/conversation-agent-loop-handlers.ts +102 -6
  83. package/src/daemon/conversation-agent-loop.ts +126 -270
  84. package/src/daemon/conversation-runtime-assembly.ts +44 -8
  85. package/src/daemon/conversation-tool-setup.ts +9 -1
  86. package/src/daemon/conversation.ts +13 -0
  87. package/src/daemon/external-plugins-bootstrap.ts +33 -87
  88. package/src/daemon/handlers/config-channels.ts +11 -2
  89. package/src/daemon/handlers/shared.ts +9 -1
  90. package/src/daemon/handlers/skills.ts +6 -2
  91. package/src/daemon/host-app-control-proxy.ts +72 -57
  92. package/src/daemon/message-protocol.ts +0 -7
  93. package/src/daemon/message-types/skills.ts +4 -0
  94. package/src/events/tool-audit-listener.ts +7 -1
  95. package/src/memory/__tests__/tool-invocation-test-helpers.ts +69 -0
  96. package/src/memory/conversation-crud.ts +44 -1
  97. package/src/memory/db-init.ts +4 -0
  98. package/src/memory/embedding-backend.ts +5 -22
  99. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  100. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  101. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  102. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  103. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  104. package/src/memory/migrations/index.ts +2 -0
  105. package/src/memory/schema/conversations.ts +3 -0
  106. package/src/memory/tool-execution-events-store.test.ts +162 -0
  107. package/src/memory/tool-execution-events-store.ts +73 -0
  108. package/src/memory/tool-usage-store.ts +4 -3
  109. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +51 -0
  110. package/src/memory/v2/activation-store.ts +50 -1
  111. package/src/memory/v2/injected-block-slugs.ts +30 -0
  112. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  113. package/src/notifications/access-request-copy.ts +298 -0
  114. package/src/notifications/adapters/slack.ts +3 -3
  115. package/src/notifications/adapters/telegram.ts +2 -1
  116. package/src/notifications/copy-composer.ts +49 -267
  117. package/src/notifications/decision-engine.ts +16 -35
  118. package/src/notifications/home-feed-side-effect.ts +1 -6
  119. package/src/platform/client.ts +1 -3
  120. package/src/platform/feature-gate.ts +3 -12
  121. package/src/plugin-api/constants.ts +2 -0
  122. package/src/plugin-api/index.ts +37 -6
  123. package/src/plugin-api/types.ts +75 -0
  124. package/src/plugins/defaults/compaction/compact.ts +39 -1
  125. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  126. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  127. package/src/plugins/defaults/compaction/window-manager.ts +195 -1
  128. package/src/plugins/defaults/index.ts +24 -0
  129. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  130. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +36 -67
  131. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  132. package/src/plugins/external-api.ts +1 -1
  133. package/src/plugins/types.ts +5 -51
  134. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  135. package/src/providers/model-catalog.ts +57 -1
  136. package/src/providers/registry.ts +0 -8
  137. package/src/providers/retry.ts +35 -7
  138. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  139. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  140. package/src/runtime/routes/__tests__/plugins-routes.test.ts +4 -4
  141. package/src/runtime/routes/acp-routes.test.ts +106 -0
  142. package/src/runtime/routes/acp-routes.ts +248 -2
  143. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  144. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  145. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  146. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  147. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  148. package/src/runtime/routes/plugins-routes.ts +3 -3
  149. package/src/runtime/verification-outbound-actions.ts +147 -2
  150. package/src/runtime/verification-templates.ts +29 -3
  151. package/src/telemetry/types.ts +25 -1
  152. package/src/telemetry/usage-telemetry-reporter.test.ts +141 -7
  153. package/src/telemetry/usage-telemetry-reporter.ts +63 -7
  154. package/src/tools/executor.ts +4 -1
  155. package/src/tools/skills/load.ts +11 -6
  156. package/src/tools/terminal/safe-env.ts +1 -0
  157. package/src/tools/types.ts +10 -0
  158. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  159. package/src/daemon/message-types/browser.ts +0 -10
  160. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  161. package/src/plugins/plugin-skill-contributions.ts +0 -292
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. 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.
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.
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
- [`experimental/plugins/README.md`](../../../../experimental/plugins/README.md).
20
- [`simple-memory`](../../../../experimental/plugins/simple-memory/) is the
19
+ [`plugins/README.md`](../../../../plugins/README.md).
20
+ [`simple-memory`](../../../../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 [`experimental/plugins/README.md`](../../../../experimental/plugins/README.md)
118
+ - Read [`plugins/README.md`](../../../../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,6 +195,8 @@ 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;
198
200
  }
199
201
 
200
202
  export interface AllowlistOption {
package/openapi.yaml CHANGED
@@ -3,7 +3,7 @@
3
3
  openapi: 3.1.0
4
4
  info:
5
5
  title: Vellum Assistant API
6
- version: 0.8.9
6
+ version: 0.8.10
7
7
  description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
8
8
  servers:
9
9
  - url: http://127.0.0.1:7821
@@ -96,8 +96,11 @@ 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 daemon
100
- restart) are transparently resumed from persisted history first, when the agent supports ACP session loading.
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."
101
104
  tags:
102
105
  - acp
103
106
  responses:
@@ -115,6 +118,11 @@ paths:
115
118
  resumed:
116
119
  description: True when the session was resumed from persisted history before steering.
117
120
  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
118
126
  required:
119
127
  - acpSessionId
120
128
  - steered
@@ -15937,6 +15945,8 @@ paths:
15937
15945
  - assistant
15938
15946
  content:
15939
15947
  type: string
15948
+ deprecated: true
15949
+ description: "Deprecated: superseded by contentBlocks. Flat plain-text body (joined text segments)."
15940
15950
  timestamp:
15941
15951
  type: string
15942
15952
  attachments:
@@ -16241,6 +16251,8 @@ paths:
16241
16251
  - name
16242
16252
  - input
16243
16253
  additionalProperties: false
16254
+ deprecated: true
16255
+ description: "Deprecated: superseded by contentBlocks (the tool_use variant). Flat list of tool calls."
16244
16256
  surfaces:
16245
16257
  type: array
16246
16258
  items:
@@ -16292,18 +16304,28 @@ paths:
16292
16304
  - surfaceType
16293
16305
  - data
16294
16306
  additionalProperties: false
16307
+ deprecated: true
16308
+ description: "Deprecated: superseded by contentBlocks (the surface variant). Flat list of surfaces."
16295
16309
  textSegments:
16296
16310
  type: array
16297
16311
  items:
16298
16312
  type: string
16313
+ deprecated: true
16314
+ description: "Deprecated: superseded by contentBlocks. Text segments split by tool-call boundaries."
16299
16315
  thinkingSegments:
16300
16316
  type: array
16301
16317
  items:
16302
16318
  type: string
16319
+ deprecated: true
16320
+ description: "Deprecated: superseded by contentBlocks. Reasoning text extracted from thinking blocks."
16303
16321
  contentOrder:
16304
16322
  type: array
16305
16323
  items:
16306
16324
  type: string
16325
+ deprecated: true
16326
+ description:
16327
+ 'Deprecated: superseded by contentBlocks. Positional "<type>:<index>" content ordering (e.g. "text:0",
16328
+ "thinking:1").'
16307
16329
  contentBlocks:
16308
16330
  type: array
16309
16331
  items:
@@ -16326,6 +16348,10 @@ paths:
16326
16348
  const: thinking
16327
16349
  thinking:
16328
16350
  type: string
16351
+ startedAt:
16352
+ type: number
16353
+ completedAt:
16354
+ type: number
16329
16355
  required:
16330
16356
  - type
16331
16357
  - thinking
@@ -19449,11 +19475,11 @@ paths:
19449
19475
  operationId: plugins_install_post
19450
19476
  summary: Install a plugin
19451
19477
  description:
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
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
19457
19483
  plugins install <name>`. An already-installed name without `force` returns 409; a name that resolves to nothing
19458
19484
  returns 404. Sibling to `POST /v1/skills/install`."
19459
19485
  tags:
@@ -19515,9 +19541,9 @@ paths:
19515
19541
  operationId: plugins_search_get
19516
19542
  summary: Search the plugin catalog
19517
19543
  description:
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`.
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`.
19521
19547
  tags:
19522
19548
  - plugins
19523
19549
  responses:
@@ -19545,7 +19571,7 @@ paths:
19545
19571
  path:
19546
19572
  type: string
19547
19573
  description:
19548
- "Human-readable origin: a repo-relative path (`experimental/plugins/<name>`) for first-party plugins or a
19574
+ "Human-readable origin: a repo-relative path (`plugins/<name>`) for first-party plugins or a
19549
19575
  `github:owner/repo@ref` locator for external ones."
19550
19576
  description:
19551
19577
  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.9",
3
+ "version": "0.8.10-dev.202606092334.09948c8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -74,6 +74,8 @@ function projectModel(model: CatalogModel): Record<string, unknown> {
74
74
  projected.longContextMode = model.longContextMode;
75
75
  if (model.supportsThinking !== undefined)
76
76
  projected.supportsThinking = model.supportsThinking;
77
+ if (model.adaptiveThinkingOnly !== undefined)
78
+ projected.adaptiveThinkingOnly = model.adaptiveThinkingOnly;
77
79
  if (model.supportsCaching !== undefined)
78
80
  projected.supportsCaching = model.supportsCaching;
79
81
  if (model.supportsVision !== undefined)
@@ -13,7 +13,9 @@
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, mock, test } from "bun:test";
16
+ import { afterEach, describe, expect, test } from "bun:test";
17
+
18
+ import type { PostCompactContext } from "@vellumai/plugin-api";
17
19
 
18
20
  import type {
19
21
  AgentEvent,
@@ -23,12 +25,13 @@ import type {
23
25
  import { AgentLoop, isMaxTokensStopReason } from "../agent/loop.js";
24
26
  import type { ContextWindowConfig } from "../config/types.js";
25
27
  import type { TrustContext } from "../daemon/trust-context.js";
28
+ import { HOOKS } from "../plugin-api/constants.js";
26
29
  import {
27
30
  createContextWindowManager,
28
31
  disposeContextWindowManager,
29
32
  getContextWindowManager,
30
33
  } from "../plugins/defaults/compaction/manager-store.js";
31
- import type { PostCompactContext } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
34
+ import { registerPlugin } from "../plugins/registry.js";
32
35
  import type {
33
36
  Message,
34
37
  Provider,
@@ -37,22 +40,25 @@ import type {
37
40
  ToolDefinition,
38
41
  } from "../providers/types.js";
39
42
 
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.
43
+ // The agent loop runs the default post-compaction re-injection through the
44
+ // `post-compact` hook chain when it compacts in place. Register a test hook on
45
+ // that chain so these unit tests can drive the re-injection result without the
46
+ // daemon-level injector. The hook writes the re-injected history back onto the
47
+ // context; tests assign `postCompactImpl` to observe the call or force a
48
+ // failure; when unset the hook leaves the history it was handed untouched.
45
49
  let postCompactImpl:
46
50
  | ((input: PostCompactContext) => Promise<Message[]>)
47
51
  | null = null;
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
- );
52
+ registerPlugin({
53
+ manifest: { name: "test-post-compact", version: "0.0.0" },
54
+ hooks: {
55
+ [HOOKS.POST_COMPACT]: async (input: PostCompactContext): Promise<void> => {
56
+ input.history = postCompactImpl
57
+ ? await postCompactImpl(input)
58
+ : input.history;
59
+ },
60
+ },
61
+ });
56
62
 
57
63
  // ---------------------------------------------------------------------------
58
64
  // Helpers (mirrored from agent-loop.test.ts so this file is self-contained)
@@ -183,6 +183,7 @@ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
183
183
  async maybeCompact() {
184
184
  return { compacted: false };
185
185
  }
186
+ resetOverflowRecovery() {}
186
187
  },
187
188
  createContextSummaryMessage: () => ({
188
189
  role: "user",
@@ -54,7 +54,10 @@ mock.module("../memory/llm-request-log-store.js", () => ({
54
54
  }));
55
55
 
56
56
  // ── Imports (after mocks) ─────────────────────────────────────────────────────
57
- import { buildPersistedAssistantContent } from "../daemon/conversation-agent-loop-handlers.js";
57
+ import {
58
+ buildPersistedAssistantContent,
59
+ stampThinkingTiming,
60
+ } from "../daemon/conversation-agent-loop-handlers.js";
58
61
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
59
62
  import type { ContentBlock } from "../providers/types.js";
60
63
 
@@ -182,3 +185,74 @@ describe("buildPersistedAssistantContent — native activityMetadata", () => {
182
185
  expect(block._activityMetadata).toBeUndefined();
183
186
  });
184
187
  });
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
+ });
@@ -198,6 +198,7 @@ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
198
198
  async maybeCompact(): Promise<ContextWindowResult> {
199
199
  return mockCompactResult;
200
200
  }
201
+ resetOverflowRecovery() {}
201
202
  },
202
203
  createContextSummaryMessage: () => ({
203
204
  role: "user",
@@ -57,17 +57,8 @@ 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
-
66
60
  mock.module("../config/assistant-feature-flags.js", () => ({
67
- isAssistantFeatureFlagEnabled: (key: string) => {
68
- if (key === "managed-gemini-embeddings-enabled") return featureFlagEnabled;
69
- return true;
70
- },
61
+ isAssistantFeatureFlagEnabled: () => true,
71
62
  clearFeatureFlagOverridesCache: () => {},
72
63
  initFeatureFlagOverrides: async () => {},
73
64
  getAssistantFeatureFlagDefaults: () => ({}),
@@ -119,8 +110,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
119
110
  setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
120
111
  invalidateConfigCache();
121
112
 
122
- // Reset mock state
123
- featureFlagEnabled = false;
124
113
  originalIsPlatform = process.env.IS_PLATFORM;
125
114
  delete process.env.IS_PLATFORM;
126
115
  });
@@ -137,10 +126,9 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
137
126
  }
138
127
  });
139
128
 
140
- test("applies managed Gemini defaults when FF on + IS_PLATFORM + provider auto", () => {
129
+ test("applies managed Gemini defaults when IS_PLATFORM + provider auto", () => {
141
130
  writeConfig({});
142
131
 
143
- featureFlagEnabled = true;
144
132
  process.env.IS_PLATFORM = "true";
145
133
 
146
134
  const config = loadConfig();
@@ -162,22 +150,9 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
162
150
  expect(qdrantRaw.vectorSize).toBe(3072);
163
151
  });
164
152
 
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
-
177
153
  test("does NOT apply when IS_PLATFORM is not set", () => {
178
154
  writeConfig({});
179
155
 
180
- featureFlagEnabled = true;
181
156
  delete process.env.IS_PLATFORM;
182
157
 
183
158
  const config = loadConfig();
@@ -191,7 +166,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
191
166
  memory: { embeddings: { provider: "local" } },
192
167
  });
193
168
 
194
- featureFlagEnabled = true;
195
169
  process.env.IS_PLATFORM = "true";
196
170
 
197
171
  const config = loadConfig();
@@ -204,7 +178,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
204
178
  memory: { embeddings: { provider: "openai" } },
205
179
  });
206
180
 
207
- featureFlagEnabled = true;
208
181
  process.env.IS_PLATFORM = "true";
209
182
 
210
183
  const config = loadConfig();
@@ -219,7 +192,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
219
192
  },
220
193
  });
221
194
 
222
- featureFlagEnabled = true;
223
195
  process.env.IS_PLATFORM = "true";
224
196
 
225
197
  const config = loadConfig();
@@ -235,7 +207,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
235
207
  memory: { embeddings: { provider: "ollama" } },
236
208
  });
237
209
 
238
- featureFlagEnabled = true;
239
210
  process.env.IS_PLATFORM = "true";
240
211
 
241
212
  const config = loadConfig();
@@ -245,7 +216,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
245
216
  test("is idempotent — second loadConfig is a no-op after migration", () => {
246
217
  writeConfig({});
247
218
 
248
- featureFlagEnabled = true;
249
219
  process.env.IS_PLATFORM = "true";
250
220
 
251
221
  const config = loadConfig();
@@ -274,7 +244,6 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
274
244
  },
275
245
  });
276
246
 
277
- featureFlagEnabled = true;
278
247
  process.env.IS_PLATFORM = "true";
279
248
 
280
249
  const config = loadConfig();
@@ -295,22 +264,9 @@ describe("managed Gemini embedding defaults (via loadConfig)", () => {
295
264
  expect(qdrantRaw.onDisk).toBe(false);
296
265
  });
297
266
 
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
-
310
267
  test("applies when IS_PLATFORM is '1'", () => {
311
268
  writeConfig({});
312
269
 
313
- featureFlagEnabled = true;
314
270
  process.env.IS_PLATFORM = "1";
315
271
 
316
272
  const config = loadConfig();