@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
@@ -0,0 +1,310 @@
1
+ # Activation Funnel Telemetry — Runbook + Analytics Handoff
2
+
3
+ > **Linear:** JARVIS-1102 (event emission) — gates JARVIS-1092 (10% rollout) and JARVIS-1093 (dashboard).
4
+ > **Funnel version:** `activation_v1_2026_06`
5
+ > **LD flag:** `experiment-activation-flow-2026-06-03`
6
+ > **Cohort arm tag:** `ab_variant = "variant-a"` (treatment); `control` = the no-rail arm.
7
+
8
+ This doc is the single executable reference for the activation-rail funnel
9
+ telemetry: what it measures, how it flows, the event vocabulary, the dedup
10
+ contract, the local smoke-test runbook, the BigQuery verification query, and the
11
+ canonical handoff blurb for the dashboard work. It is meant to be runnable by
12
+ another engineer without reading the implementation.
13
+
14
+ ---
15
+
16
+ ## 1. Overview — what it measures and how it flows
17
+
18
+ The activation funnel measures how far a new user gets on their first run of the
19
+ **activation rail** (the onboarding experience driven by
20
+ `BOOTSTRAP-ACTIVATION-RAIL.md`). It emits five milestone ("funnel") events per
21
+ session into the **existing onboarding telemetry substrate** — no new event type,
22
+ no new ingest endpoint, no new BigQuery table.
23
+
24
+ The north-star "≥5 user messages within 24h per LD variant" metric is **not** a
25
+ materialized activation event. It is computed downstream by joining the existing
26
+ per-user-message `turn` telemetry events (`turn_index` per conversation) to the
27
+ platform's `flag_assignment_raw` cohort-assignment table — so a dedicated event
28
+ and a custom daemon turn-counting hook are not needed.
29
+
30
+ Flow, end to end:
31
+
32
+ 1. **Daemon records** an activation event into the SQLite `onboarding_events`
33
+ table via `recordActivationEvent()` in
34
+ `assistant/src/memory/onboarding-events-store.ts`:
35
+ - emission is **deterministic, tied to a `ui_show` surface** — there is no
36
+ model-facing tool. The model tags the surface it is already rendering for a
37
+ rail move with an optional `activation_moment` parameter on `ui_show`; the
38
+ daemon captures that tag on the surface's server-side state and records the
39
+ milestone (gated on `isActivationSession`). **Timing is per-moment**
40
+ (`ACTIVATION_MOMENT_EMIT_AT` in `activation-funnel.ts`): most moments record
41
+ when the user **commits** the surface (clicks an action / submits /
42
+ selects) via `handleSurfaceAction()`; the one exception is
43
+ `first_wow_executed`, which records at **render time** in
44
+ `surfaceProxyResolver` — the Run result/`work_result` surface is often
45
+ display-only and may never be committed, so a commit-time emit would never
46
+ fire (and deferring it to a later click would conflate "executed" with
47
+ "interacted"). Show-timing tags are recorded immediately and not stored, so
48
+ the commit path never double-emits. The token→step-name map and the
49
+ show/commit timing both live in
50
+ `assistant/src/telemetry/activation-funnel.ts`;
51
+ - emission is best-effort (wrapped in try/catch) and never blocks or alters
52
+ the surface-action flow;
53
+ - `recordActivationEvent` respects the `getConfig().collectUsageData` opt-out
54
+ gate (returns `null` / no row when disabled).
55
+ 2. **Reporter flushes** every ~5 min: `usage-telemetry-reporter.ts`
56
+ (`REPORT_INTERVAL_MS = 5 * 60 * 1000`, with a one-time
57
+ `INITIAL_FLUSH_DELAY_MS = 30_000` after startup) POSTs unreported onboarding
58
+ rows to `/v1/telemetry/ingest/` as `type: "onboarding"` events, mapping the
59
+ funnel columns onto the wire shape.
60
+ 3. **Platform ingests** the onboarding events and writes GCS NDJSON.
61
+ 4. **BigQuery** exposes them via the external table
62
+ `vellum-ai-prod.telemetry.onboarding_raw`, which already carries the
63
+ `session_id / step_name / step_index / completed_at / funnel_version /
64
+ ab_variant` columns. The existing dbt model dedups on `daemon_event_id`
65
+ (earliest-wins).
66
+
67
+ No platform / dbt / terraform change is required — the activation events ride the
68
+ `type: "onboarding"` substrate that already exists end to end.
69
+
70
+ ---
71
+
72
+ ## 2. Event vocabulary
73
+
74
+ The single source of truth is `assistant/src/telemetry/activation-funnel.ts`
75
+ (`ACTIVATION_STEPS`). `funnel_version = "activation_v1_2026_06"`
76
+ (`ACTIVATION_FUNNEL_VERSION`). `ab_variant = "variant-a"`
77
+ (`ACTIVATION_AB_VARIANT`, the treatment arm).
78
+
79
+ Each step is recorded when the user commits the `ui_show` surface the model
80
+ tagged with the corresponding `activation_moment` token (see §1). The
81
+ token→step-name map is the `moment_*` column below.
82
+
83
+ | step_index | step_name | `activation_moment` token | Recorded on commit of |
84
+ | ---------- | --------------------------------- | ------------------------- | --------------------------------------------------------------- |
85
+ | 1 | `activation_moment_1_complete` | `moment_1` | Port-summary card OR no-port intake `choice` surface |
86
+ | 2 | `activation_moment_2_complete` | `moment_2` | Propose offer surface (the `ui_show` offer card/choice) |
87
+ | 3 | `activation_moment_3_complete` | `moment_3` | task-selection surface |
88
+ | 4 | `activation_first_wow_executed` | `first_wow_executed` | Run result surface (e.g. `work_result`) |
89
+ | 5 | `activation_first_wow_interacted` | `first_wow_interacted` | user clicks an action on the tagged result surface (see §1, §4) |
90
+
91
+ All five steps are recorded deterministically on surface commit. The north star
92
+ (≥5 user messages) is derived downstream from the existing `turn` telemetry, not
93
+ a materialized activation event (see §1).
94
+
95
+ On the wire, each onboarding event also sets `screen = step_name` (to satisfy the
96
+ SQLite `screen TEXT NOT NULL` column and the platform's legacy-path validation),
97
+ and `completed_at` is the ISO-8601 record time.
98
+
99
+ ---
100
+
101
+ ## 3. Dedup contract
102
+
103
+ Activation rows carry a **deterministic** `daemon_event_id`:
104
+
105
+ ```
106
+ daemon_event_id = `${funnel_version}:${session_id}:${step_name}`
107
+ ```
108
+
109
+ Built by `buildActivationDaemonEventId()` in
110
+ `assistant/src/telemetry/activation-funnel.ts`. The reporter overrides
111
+ `daemon_event_id` for activation rows (where `session_id && step_name &&
112
+ funnel_version` are all present) and keys it on the **row's stored
113
+ `funnel_version`**, NOT the running binary's current constant. This keeps the id
114
+ stable across a version bump so rows queued offline / flushed after an upgrade
115
+ still collapse with already-ingested rows from the same session.
116
+
117
+ A moment that fires more than once (e.g. a model double-emit) therefore lands
118
+ with the same `daemon_event_id` and is collapsed downstream by the existing dbt
119
+ earliest-wins dedup on `daemon_event_id`. For boolean "moment complete"
120
+ semantics, earliest-wins is correct.
121
+
122
+ **Checkpoint safety:** the SQLite watermark cursor in the reporter advances on the
123
+ row `id` / `createdAt`, NOT on `daemon_event_id`. The deterministic id is a
124
+ wire-only override, so overriding it never affects flush checkpointing.
125
+
126
+ ---
127
+
128
+ ## 4. Cohort scoping
129
+
130
+ Events only emit for conversations explicitly marked as **activation-rail
131
+ sessions**:
132
+
133
+ - The marker lives in the `activation_sessions` table
134
+ (`assistant/src/memory/activation-session-store.ts`,
135
+ `markActivationSession` / `isActivationSession`).
136
+ - It is set in `assistant/src/prompts/system-prompt.ts` **only when the
137
+ `BOOTSTRAP-ACTIVATION-RAIL.md` template is actually active** (i.e.
138
+ `bootstrapTemplate === ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE`), which the web
139
+ prechat context selects only for users whose
140
+ `experiment-activation-flow-2026-06-03` cohort puts them on the rail. (It is
141
+ also set up-front in `conversation.ts` `setOnboardingContext` so the marker is
142
+ available before the first turn's tool resolution.)
143
+ - Emission is **gated on the marker at commit time**: when a tagged `ui_show`
144
+ surface is committed, the daemon records the milestone only if
145
+ `isActivationSession(conversationId)` is true. A stray `activation_moment` tag
146
+ in a normal chat is therefore ignored and never pollutes the funnel. (`moment_4`
147
+ / `first_wow_interacted` deduping is handled the same way as any other moment —
148
+ see the dedup contract in §3 — and the per-surface tag is cleared after the
149
+ first record so a single surface commit emits at most once.)
150
+
151
+ The activation **session id is the daemon `conversation_id`** of the rail
152
+ conversation. That same value is `session_id` on every activation event.
153
+
154
+ ---
155
+
156
+ ## 5. Local smoke-test runbook
157
+
158
+ Goal: drive one dev session through the activation cohort, emit all five events,
159
+ flush, and verify rows in BigQuery. Executable by another engineer.
160
+
161
+ ### 5.1 Put the dev user in the activation cohort
162
+
163
+ The rail is gated by the LD flag `experiment-activation-flow-2026-06-03`. Two
164
+ options:
165
+
166
+ - **In-app feature-flags override (fastest):** open the in-app Feature Flags
167
+ panel and force `experiment-activation-flow-2026-06-03` ON for the dev user.
168
+ - **LaunchDarkly targeting:** target the dev user for the flag in the LD
169
+ dashboard (per the LD-flag-two-repo convention, targeting is dashboard-side).
170
+
171
+ When the flag is ON, the web prechat context selects
172
+ `BOOTSTRAP-ACTIVATION-RAIL.md` as the bootstrap template, and the daemon marks
173
+ the conversation in `activation_sessions` on first build of the system prompt.
174
+
175
+ ### 5.2 Confirm usage-data collection is enabled — and do NOT run in dev mode
176
+
177
+ Two gates must both be satisfied or no rows reach BigQuery:
178
+
179
+ 1. `recordActivationEvent` no-ops when `config.collectUsageData` is false, so the
180
+ dev build/config must have usage-data collection enabled, or no rows are
181
+ written to SQLite.
182
+ 2. **Dev mode disables the flush entirely.** `assistant/src/daemon/lifecycle.ts`
183
+ computes the effective setting as `collectUsageData = !isDevMode &&
184
+ config.collectUsageData`, so when the daemon runs in dev mode (`VELLUM_DEV=1`)
185
+ the `UsageTelemetryReporter` is never started — rows accumulate in SQLite but
186
+ are never POSTed, and the "wait/restart for flush" steps below will never
187
+ reach BigQuery. For an end-to-end smoke test, run the daemon **outside dev
188
+ mode** (so the reporter starts), or explicitly invoke the reporter's
189
+ `flush()` via a dev hook. The SQLite rows can still be inspected directly in
190
+ dev mode, but the BigQuery verification (§6) requires a real flush.
191
+
192
+ ### 5.3 Start a fresh activation conversation and capture its id
193
+
194
+ Start a brand-new conversation with the flag ON (it must be the rail bootstrap, so
195
+ start it from the web prechat flow / activation entry, not an existing chat).
196
+ Note its `conversation_id` — this is the `session_id` you will query on.
197
+
198
+ If multiple daemons are running, kill ALL `daemon/main.ts` processes, remove
199
+ `~/.vellum/vellum.sock`, and start fresh, so the conversation is served by a known
200
+ daemon writing to a known SQLite DB.
201
+
202
+ ### 5.4 Drive the rail through all five model moments
203
+
204
+ Work the conversation through the rail moves so the model tags each surface and
205
+ you commit it (the model tags surfaces via `ui_show`'s `activation_moment`
206
+ parameter; which surface maps to which moment is documented in
207
+ `BOOTSTRAP-ACTIVATION-RAIL.md` and §2). Each milestone records when YOU commit
208
+ the tagged surface:
209
+
210
+ 1. Commit the Port-summary card / no-port intake `choice` → `activation_moment_1_complete`.
211
+ 2. Commit the Propose offer surface → `activation_moment_2_complete`.
212
+ 3. Commit the task-selection surface → `activation_moment_3_complete`.
213
+ 4. Commit the Run result surface → `activation_first_wow_executed`.
214
+ 5. Click an action on the result surface → `activation_first_wow_interacted`.
215
+
216
+ ### 5.5 Force / await a telemetry flush
217
+
218
+ The reporter flushes on a schedule: a first flush ~30s after startup
219
+ (`INITIAL_FLUSH_DELAY_MS`) and then every ~5 min (`REPORT_INTERVAL_MS`). Either:
220
+
221
+ - **Wait** for the next scheduled flush (≤ ~5 min), or
222
+ - **Restart the daemon** to take the ~30s post-startup flush, or
223
+ - call the reporter's `flush()` path directly if you have a dev hook into the
224
+ running daemon.
225
+
226
+ A successful flush POSTs the unreported onboarding rows to
227
+ `/v1/telemetry/ingest/` as `type: "onboarding"` events.
228
+
229
+ ### 5.6 Verify in BigQuery
230
+
231
+ Wait for the platform → GCS NDJSON → BigQuery pipeline to land the events, then
232
+ run the query in §6.
233
+
234
+ ---
235
+
236
+ ## 6. BigQuery verification query
237
+
238
+ ```sql
239
+ SELECT step_name, step_index, COUNT(*) AS n
240
+ FROM `vellum-ai-prod.telemetry.onboarding_raw`
241
+ WHERE funnel_version = 'activation_v1_2026_06'
242
+ AND session_id = '<dev conversation id>'
243
+ GROUP BY 1, 2
244
+ ORDER BY 2;
245
+ ```
246
+
247
+ **Expected:** all five `activation_*` step names for the one session, step_index
248
+ 1–5, each with `n = 1`:
249
+
250
+ ```
251
+ activation_moment_1_complete | 1 | 1
252
+ activation_moment_2_complete | 2 | 1
253
+ activation_moment_3_complete | 3 | 1
254
+ activation_first_wow_executed | 4 | 1
255
+ activation_first_wow_interacted | 5 | 1
256
+ ```
257
+
258
+ A missing row means that milestone never emitted (re-drive that rail move); an
259
+ `n > 1` before dbt dedup is harmless (the deterministic `daemon_event_id`
260
+ collapses it earliest-wins).
261
+
262
+ ---
263
+
264
+ ## 7. Canonical handoff blurb (paste-ready for the final JARVIS-1102 PR description)
265
+
266
+ > **Activation funnel telemetry — handoff for JARVIS-1093.** The activation rail
267
+ > now emits five milestone funnel events into the existing onboarding telemetry
268
+ > substrate (`type: "onboarding"` → `/v1/telemetry/ingest/` → GCS NDJSON →
269
+ > `vellum-ai-prod.telemetry.onboarding_raw`), so no new event type, ingest
270
+ > endpoint, or BigQuery table is involved. Cohort is gated by the LaunchDarkly
271
+ > flag **`experiment-activation-flow-2026-06-03`**; only rail (treatment)
272
+ > conversations emit. Every event carries **`funnel_version =
273
+ "activation_v1_2026_06"`** and **`ab_variant`** tagged `"variant-a"` for
274
+ > treatment (`"control"` is the no-rail arm, measured for now from generic `turn`
275
+ > events). The event vocabulary (step_name → step_index): `activation_moment_1_complete`
276
+ > (1), `activation_moment_2_complete` (2), `activation_moment_3_complete` (3),
277
+ > `activation_first_wow_executed` (4), `activation_first_wow_interacted` (5).
278
+ > `session_id` is the rail conversation's id. The **north star** ("≥5 user
279
+ > messages within 24h per LD variant") is **not** a funnel event — compute it
280
+ > downstream by joining the existing `turn` telemetry events (`turn_index` per
281
+ > conversation) to the platform's `flag_assignment_raw` cohort-assignment table.
282
+ > **Dedup contract:** each row's `daemon_event_id` is deterministic,
283
+ > `${funnel_version}:${session_id}:${step_name}`, keyed on the row's stored
284
+ > `funnel_version`; the existing dbt earliest-wins dedup on `daemon_event_id`
285
+ > collapses any repeated moment to a single row. Build funnel conversion as a
286
+ > step_index 1→5 progression per `session_id`, filtered to `funnel_version =
287
+ 'activation_v1_2026_06'`.
288
+
289
+ ---
290
+
291
+ ## 8. Notes
292
+
293
+ - **JARVIS-1102 "naming check" (resolved).** Events fire **deterministically on
294
+ the real user commit of a `ui_show` surface, not every text turn**. The model
295
+ passively tags the surface for a rail move with `activation_moment`; the daemon
296
+ records the milestone in `handleSurfaceAction` when the user commits that
297
+ surface (firing conditions / surface→moment mapping documented in
298
+ `BOOTSTRAP-ACTIVATION-RAIL.md`). This removes the standalone
299
+ `emit_activation_event` tool (and its cohort preactivation) entirely and ties
300
+ emission to a genuine user action, resolving the naming-check open
301
+ interpretation.
302
+ - **Stream B (multivariate cohort flag conversion) is NOT in this work.** It is
303
+ gated on the platform team's in-flight string-flag serving — until the platform
304
+ can serve string/multivariate flags, flipping
305
+ `experiment-activation-flow-2026-06-03` boolean→multivariate client-side would
306
+ silently disable the live allowlisted rail. Until then, `ab_variant` is the
307
+ constant `"variant-a"` (only treatment runs the rail), and the **control side**
308
+ of the comparison is measured from generic `turn` events split by the
309
+ platform's experiment exposure mapping. Once Stream B lands, the daemon tags the
310
+ real assigned arm and the cross-cohort comparison works with zero rework.
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.8
6
+ version: 0.8.9
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
@@ -480,6 +480,8 @@ paths:
480
480
  type: string
481
481
  createdAt:
482
482
  type: number
483
+ updatedAt:
484
+ type: number
483
485
  version:
484
486
  type: string
485
487
  contentId:
@@ -488,6 +490,7 @@ paths:
488
490
  - id
489
491
  - name
490
492
  - createdAt
493
+ - updatedAt
491
494
  - version
492
495
  - contentId
493
496
  additionalProperties: false
@@ -15854,10 +15857,10 @@ paths:
15854
15857
  type: object
15855
15858
  properties: {}
15856
15859
  additionalProperties: false
15857
- /v1/memory/v3/health:
15860
+ /v1/memory/v3/backfill-sections:
15858
15861
  post:
15859
- operationId: memory_v3_health_post
15860
- summary: Print the v3 structural health report (read-only)
15862
+ operationId: memory_v3_backfillsections_post
15863
+ summary: "One-time: embed every page's sections (incl synthetic skill/CLI rows) into the dense store"
15861
15864
  tags:
15862
15865
  - memory
15863
15866
  responses:
@@ -15868,31 +15871,16 @@ paths:
15868
15871
  schema:
15869
15872
  type: object
15870
15873
  properties:
15871
- rendered:
15872
- type: string
15873
- counts:
15874
- type: object
15875
- properties:
15876
- unassigned:
15877
- type: number
15878
- danglingRefs:
15879
- type: number
15880
- novelClusters:
15881
- type: number
15882
- oversizedLeaves:
15883
- type: number
15884
- tinyLeaves:
15885
- type: number
15886
- required:
15887
- - unassigned
15888
- - danglingRefs
15889
- - novelClusters
15890
- - oversizedLeaves
15891
- - tinyLeaves
15892
- additionalProperties: false
15874
+ articles:
15875
+ type: number
15876
+ sections:
15877
+ type: number
15878
+ failures:
15879
+ type: number
15893
15880
  required:
15894
- - rendered
15895
- - counts
15881
+ - articles
15882
+ - sections
15883
+ - failures
15896
15884
  additionalProperties: false
15897
15885
  /v1/memory/v3/rebuild-index:
15898
15886
  post:
@@ -15914,93 +15902,6 @@ paths:
15914
15902
  required:
15915
15903
  - ok
15916
15904
  additionalProperties: false
15917
- /v1/memory/v3/reconcile:
15918
- post:
15919
- operationId: memory_v3_reconcile_post
15920
- summary: v1 convergence/prune pass over page+core refs (no rename detection without a prior snapshot)
15921
- tags:
15922
- - memory
15923
- responses:
15924
- "200":
15925
- description: Successful response
15926
- content:
15927
- application/json:
15928
- schema:
15929
- type: object
15930
- properties:
15931
- renames:
15932
- type: array
15933
- items:
15934
- type: object
15935
- properties:
15936
- id:
15937
- type: string
15938
- oldPath:
15939
- type: string
15940
- newPath:
15941
- type: string
15942
- required:
15943
- - oldPath
15944
- - newPath
15945
- additionalProperties: false
15946
- deleted:
15947
- type: array
15948
- items:
15949
- type: string
15950
- prunedCore:
15951
- type: array
15952
- items:
15953
- type: string
15954
- required:
15955
- - renames
15956
- - deleted
15957
- - prunedCore
15958
- additionalProperties: false
15959
- /v1/memory/v3/set-core:
15960
- post:
15961
- operationId: memory_v3_setcore_post
15962
- summary: Add/remove always-on core leaves (validates + previews cost)
15963
- tags:
15964
- - memory
15965
- responses:
15966
- "200":
15967
- description: Successful response
15968
- content:
15969
- application/json:
15970
- schema:
15971
- type: object
15972
- properties:
15973
- nextCore:
15974
- type: array
15975
- items:
15976
- type: string
15977
- alwaysOnPageCount:
15978
- type: number
15979
- written:
15980
- type: boolean
15981
- required:
15982
- - nextCore
15983
- - alwaysOnPageCount
15984
- - written
15985
- additionalProperties: false
15986
- requestBody:
15987
- required: true
15988
- content:
15989
- application/json:
15990
- schema:
15991
- type: object
15992
- properties:
15993
- add:
15994
- type: array
15995
- items:
15996
- type: string
15997
- remove:
15998
- type: array
15999
- items:
16000
- type: string
16001
- write:
16002
- type: boolean
16003
- additionalProperties: false
16004
15905
  /v1/messages:
16005
15906
  get:
16006
15907
  operationId: messages_get
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.9-staging.2",
3
+ "version": "0.8.9-staging.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Early activation-session marking.
3
+ *
4
+ * The agent loop resolves TOOLS before the SYSTEM PROMPT, so the activation
5
+ * marker must be written before tool resolution on the first activation-rail
6
+ * turn — otherwise the `activation` skill is not preactivated and the emit
7
+ * handler is gated off for the very turn that emits Moment 1. The marking lives
8
+ * in `applyBootstrapTemplate`, which is called from `setOnboardingContext` (the
9
+ * earliest point the conversation knows its bootstrap selection) as well as
10
+ * from `buildSystemPrompt` as an idempotent backstop.
11
+ *
12
+ * These tests exercise `applyBootstrapTemplate` directly against the real
13
+ * DB-backed `isActivationSession` and a real workspace BOOTSTRAP.md, proving the
14
+ * marker is set INDEPENDENT of any `buildSystemPrompt` render.
15
+ */
16
+
17
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
20
+
21
+ const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
22
+
23
+ mock.module("../util/logger.js", () => ({
24
+ getLogger: () =>
25
+ new Proxy({} as Record<string, unknown>, {
26
+ get: () => () => {},
27
+ }),
28
+ }));
29
+
30
+ const { applyBootstrapTemplate } = await import("../prompts/system-prompt.js");
31
+ const { isActivationSession } =
32
+ await import("../memory/activation-session-store.js");
33
+ const { ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE } =
34
+ await import("../telemetry/activation-funnel.js");
35
+ const { getDb } = await import("../memory/db-connection.js");
36
+ const { initializeDb } = await import("../memory/db-init.js");
37
+ const { activationSessions } = await import("../memory/schema.js");
38
+
39
+ initializeDb();
40
+
41
+ /** Seed the workspace BOOTSTRAP.md with the unmodified generic template so the
42
+ * one-shot reseed inside `applyBootstrapTemplate` fires. */
43
+ function seedGenericBootstrap(): void {
44
+ mkdirSync(TEST_DIR, { recursive: true });
45
+ const generic = readFileSync(
46
+ join(import.meta.dirname, "..", "prompts", "templates", "BOOTSTRAP.md"),
47
+ "utf-8",
48
+ );
49
+ writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), generic, "utf-8");
50
+ }
51
+
52
+ describe("applyBootstrapTemplate — early activation marking", () => {
53
+ beforeEach(() => {
54
+ getDb().delete(activationSessions).run();
55
+ });
56
+
57
+ test("marks the conversation when the activation-rail template is applied", () => {
58
+ seedGenericBootstrap();
59
+ expect(isActivationSession("rail-conv")).toBe(false);
60
+
61
+ // No buildSystemPrompt call — marking must happen here.
62
+ applyBootstrapTemplate(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE, "rail-conv");
63
+
64
+ expect(isActivationSession("rail-conv")).toBe(true);
65
+ });
66
+
67
+ test("does not mark when a non-rail bootstrap template is applied", () => {
68
+ seedGenericBootstrap();
69
+
70
+ applyBootstrapTemplate("BOOTSTRAP-REFERENCE.md", "non-rail-conv");
71
+
72
+ expect(isActivationSession("non-rail-conv")).toBe(false);
73
+ });
74
+
75
+ test("does not mark when no conversation id is supplied", () => {
76
+ seedGenericBootstrap();
77
+
78
+ applyBootstrapTemplate(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE);
79
+
80
+ // Nothing to assert by id; confirm the table stayed empty.
81
+ const rows = getDb().select().from(activationSessions).all();
82
+ expect(rows.length).toBe(0);
83
+ });
84
+
85
+ test("does not mark when BOOTSTRAP.md is customized to something else", () => {
86
+ mkdirSync(TEST_DIR, { recursive: true });
87
+ // A user-customized BOOTSTRAP.md is neither the generic template (so the
88
+ // reseed no-ops) nor the activation-rail template — the rail is not active.
89
+ writeFileSync(
90
+ join(TEST_DIR, "BOOTSTRAP.md"),
91
+ "# My custom bootstrap\n",
92
+ "utf-8",
93
+ );
94
+
95
+ applyBootstrapTemplate(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE, "custom-conv");
96
+
97
+ expect(isActivationSession("custom-conv")).toBe(false);
98
+ });
99
+
100
+ test("marks idempotently when BOOTSTRAP.md already holds the rail template", () => {
101
+ mkdirSync(TEST_DIR, { recursive: true });
102
+ // Simulate a later turn: BOOTSTRAP.md already holds the rail template, so
103
+ // the reseed no-ops but marking still fires (idempotent backstop).
104
+ copyFileSync(
105
+ join(
106
+ import.meta.dirname,
107
+ "..",
108
+ "prompts",
109
+ "templates",
110
+ ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE,
111
+ ),
112
+ join(TEST_DIR, "BOOTSTRAP.md"),
113
+ );
114
+
115
+ applyBootstrapTemplate(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE, "rail-conv-2");
116
+ applyBootstrapTemplate(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE, "rail-conv-2");
117
+
118
+ expect(isActivationSession("rail-conv-2")).toBe(true);
119
+ });
120
+ });