@vellumai/assistant 0.8.11 → 0.8.12-staging.2

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 (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -1,292 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
-
5
- import { getWorkspacePromptPath } from "../util/platform.js";
6
-
7
- // ── fs.readFileSync override (for read-failure test) ─────────────────
8
- // We mock node:fs so we can inject a readFileSync that throws for the
9
- // workspace path. All other call sites fall through to the real fs.
10
- const realReadFileSync = readFileSync;
11
- const realExistsSync = existsSync;
12
-
13
- let readFileSyncOverride:
14
- | ((path: Parameters<typeof readFileSync>[0]) => string | undefined)
15
- | null = null;
16
-
17
- mock.module("node:fs", () => ({
18
- existsSync: realExistsSync,
19
- readFileSync: ((
20
- path: Parameters<typeof readFileSync>[0],
21
- opts?: Parameters<typeof readFileSync>[1],
22
- ) => {
23
- if (readFileSyncOverride) {
24
- const override = readFileSyncOverride(path);
25
- if (override !== undefined) return override;
26
- }
27
- return realReadFileSync(path, opts);
28
- }) as typeof readFileSync,
29
- }));
30
-
31
- // ── In-memory checkpoint store ───────────────────────────────────────
32
- const store = new Map<string, string>();
33
- let setCheckpointCallCount = 0;
34
-
35
- mock.module("../memory/checkpoints.js", () => ({
36
- getMemoryCheckpoint: (key: string) => store.get(key) ?? null,
37
- setMemoryCheckpoint: (key: string, value: string) => {
38
- setCheckpointCallCount += 1;
39
- store.set(key, value);
40
- },
41
- }));
42
-
43
- // ── Mutable config stub ──────────────────────────────────────────────
44
- const updatesConfig = { enabled: true };
45
-
46
- mock.module("../config/loader.js", () => ({
47
- getConfig: () => ({ updates: updatesConfig }),
48
- }));
49
-
50
- // ── pre-first-message gate stub ──────────────────────────────────────
51
- // Default: gate open (user has interacted) so the existing happy/sad
52
- // paths exercise the bulletin logic. A dedicated test below flips this
53
- // to false to assert the gate trips.
54
- let preFirstMessageGateOpen = true;
55
-
56
- mock.module("../runtime/pre-first-message-gate.js", () => ({
57
- hasReceivedUserMessage: () => preFirstMessageGateOpen,
58
- }));
59
-
60
- // ── runBackgroundJob mock ────────────────────────────────────────────
61
- let runBackgroundJobCalls = 0;
62
- let runBackgroundJobLastArgs: Record<string, unknown> | null = null;
63
- let runBackgroundJobShouldThrow = false;
64
- let runBackgroundJobOk = true;
65
- let runBackgroundJobErrorKind:
66
- | "timeout"
67
- | "model_provider"
68
- | "exception"
69
- | undefined = undefined;
70
- let runBackgroundJobErrorMessage: string | undefined = undefined;
71
- // A side-effect function invoked during the job. Lets tests simulate the
72
- // agent deleting UPDATES.md while the job is running.
73
- let runBackgroundJobSideEffect: (() => void) | null = null;
74
-
75
- mock.module("../runtime/background-job-runner.js", () => ({
76
- runBackgroundJob: async (opts: Record<string, unknown>) => {
77
- runBackgroundJobCalls += 1;
78
- runBackgroundJobLastArgs = opts;
79
- if (runBackgroundJobSideEffect) {
80
- runBackgroundJobSideEffect();
81
- }
82
- if (runBackgroundJobShouldThrow) {
83
- throw new Error("simulated runner failure");
84
- }
85
- if (runBackgroundJobOk) {
86
- return {
87
- conversationId: `conv-${runBackgroundJobCalls}`,
88
- ok: true,
89
- };
90
- }
91
- return {
92
- conversationId: `conv-${runBackgroundJobCalls}`,
93
- ok: false,
94
- error: new Error(runBackgroundJobErrorMessage ?? "simulated failure"),
95
- errorKind: runBackgroundJobErrorKind ?? "exception",
96
- };
97
- },
98
- }));
99
-
100
- const { runUpdateBulletinJobIfNeeded } =
101
- await import("../prompts/update-bulletin-job.js");
102
-
103
- const HASH_CHECKPOINT_KEY = "updates:last_processed_hash";
104
- const EMPTY_HASH = "empty";
105
-
106
- function sha256(input: string): string {
107
- return createHash("sha256").update(input).digest("hex");
108
- }
109
-
110
- const workspacePath = getWorkspacePromptPath("UPDATES.md");
111
-
112
- describe("runUpdateBulletinJobIfNeeded", () => {
113
- beforeEach(() => {
114
- store.clear();
115
- setCheckpointCallCount = 0;
116
- runBackgroundJobCalls = 0;
117
- runBackgroundJobLastArgs = null;
118
- runBackgroundJobShouldThrow = false;
119
- runBackgroundJobOk = true;
120
- runBackgroundJobErrorKind = undefined;
121
- runBackgroundJobErrorMessage = undefined;
122
- runBackgroundJobSideEffect = null;
123
- readFileSyncOverride = null;
124
- updatesConfig.enabled = true;
125
- preFirstMessageGateOpen = true;
126
- if (existsSync(workspacePath)) {
127
- rmSync(workspacePath);
128
- }
129
- });
130
-
131
- afterEach(() => {
132
- if (existsSync(workspacePath)) {
133
- rmSync(workspacePath);
134
- }
135
- });
136
-
137
- test("config disabled — no job, no checkpoint change", async () => {
138
- updatesConfig.enabled = false;
139
- writeFileSync(workspacePath, "## Real content", "utf-8");
140
-
141
- await runUpdateBulletinJobIfNeeded();
142
-
143
- expect(runBackgroundJobCalls).toBe(0);
144
- expect(setCheckpointCallCount).toBe(0);
145
- expect(store.has(HASH_CHECKPOINT_KEY)).toBe(false);
146
- });
147
-
148
- test("pre-first-message gate closed — no job, checkpoint left UNCHANGED so the job retries after the user interacts", async () => {
149
- preFirstMessageGateOpen = false;
150
- writeFileSync(workspacePath, "## Real content", "utf-8");
151
-
152
- await runUpdateBulletinJobIfNeeded();
153
-
154
- expect(runBackgroundJobCalls).toBe(0);
155
- expect(setCheckpointCallCount).toBe(0);
156
- expect(store.has(HASH_CHECKPOINT_KEY)).toBe(false);
157
- });
158
-
159
- test("file missing, stored hash absent — no job; stored becomes 'empty'", async () => {
160
- expect(existsSync(workspacePath)).toBe(false);
161
- expect(store.has(HASH_CHECKPOINT_KEY)).toBe(false);
162
-
163
- await runUpdateBulletinJobIfNeeded();
164
-
165
- expect(runBackgroundJobCalls).toBe(0);
166
- expect(store.get(HASH_CHECKPOINT_KEY)).toBe(EMPTY_HASH);
167
- });
168
-
169
- test("file missing, stored hash already 'empty' — no job; no checkpoint write", async () => {
170
- store.set(HASH_CHECKPOINT_KEY, EMPTY_HASH);
171
- setCheckpointCallCount = 0;
172
-
173
- await runUpdateBulletinJobIfNeeded();
174
-
175
- expect(runBackgroundJobCalls).toBe(0);
176
- expect(setCheckpointCallCount).toBe(0);
177
- expect(store.get(HASH_CHECKPOINT_KEY)).toBe(EMPTY_HASH);
178
- });
179
-
180
- test("file present but whitespace-only — treated as empty; stored hash 'empty'", async () => {
181
- writeFileSync(workspacePath, " \n\n\t\n", "utf-8");
182
-
183
- await runUpdateBulletinJobIfNeeded();
184
-
185
- expect(runBackgroundJobCalls).toBe(0);
186
- expect(store.get(HASH_CHECKPOINT_KEY)).toBe(EMPTY_HASH);
187
- });
188
-
189
- test("file present, job ok=true, file unchanged — stored hash is sha256(trimmed); jobName/source are kebab-case", async () => {
190
- const content = "## Release 1.2.3\n\nNew thing.\n";
191
- writeFileSync(workspacePath, content, "utf-8");
192
-
193
- await runUpdateBulletinJobIfNeeded();
194
-
195
- expect(runBackgroundJobCalls).toBe(1);
196
- expect(store.get(HASH_CHECKPOINT_KEY)).toBe(sha256(content.trim()));
197
- expect(runBackgroundJobLastArgs?.jobName).toBe("update-bulletin");
198
- expect(runBackgroundJobLastArgs?.source).toBe("update-bulletin");
199
- expect(runBackgroundJobLastArgs?.origin).toBe("updates_bulletin");
200
- expect(runBackgroundJobLastArgs?.callSite).toBe("mainAgent");
201
- });
202
-
203
- test("file present, stored hash matches current — no job", async () => {
204
- const content = "## Release 1.2.3\n\nSame content.\n";
205
- writeFileSync(workspacePath, content, "utf-8");
206
- store.set(HASH_CHECKPOINT_KEY, sha256(content.trim()));
207
- setCheckpointCallCount = 0;
208
-
209
- await runUpdateBulletinJobIfNeeded();
210
-
211
- expect(runBackgroundJobCalls).toBe(0);
212
- expect(setCheckpointCallCount).toBe(0);
213
- });
214
-
215
- test("runBackgroundJob returns ok=false — checkpoint UNCHANGED so next startup retries", async () => {
216
- const content = "## Release Q\n\nFailure scenario.\n";
217
- writeFileSync(workspacePath, content, "utf-8");
218
- runBackgroundJobOk = false;
219
- runBackgroundJobErrorKind = "exception";
220
- runBackgroundJobErrorMessage = "boom";
221
-
222
- await runUpdateBulletinJobIfNeeded();
223
-
224
- expect(runBackgroundJobCalls).toBe(1);
225
- // Critical: do NOT poison the checkpoint when the job fails.
226
- expect(store.has(HASH_CHECKPOINT_KEY)).toBe(false);
227
- expect(setCheckpointCallCount).toBe(0);
228
- });
229
-
230
- test("runBackgroundJob ok=true + agent deletes file mid-run — stored hash becomes 'empty'", async () => {
231
- const content = "## Release X\n\nStuff to process.\n";
232
- writeFileSync(workspacePath, content, "utf-8");
233
- runBackgroundJobSideEffect = () => {
234
- rmSync(workspacePath);
235
- };
236
-
237
- await runUpdateBulletinJobIfNeeded();
238
-
239
- expect(runBackgroundJobCalls).toBe(1);
240
- expect(existsSync(workspacePath)).toBe(false);
241
- expect(store.get(HASH_CHECKPOINT_KEY)).toBe(EMPTY_HASH);
242
- });
243
-
244
- test("file present, stored hash differs — job invoked; stored hash updates", async () => {
245
- const oldContent = "## Old";
246
- const newContent = "## New content v2";
247
- writeFileSync(workspacePath, newContent, "utf-8");
248
- store.set(HASH_CHECKPOINT_KEY, sha256(oldContent));
249
-
250
- await runUpdateBulletinJobIfNeeded();
251
-
252
- expect(runBackgroundJobCalls).toBe(1);
253
- expect(store.get(HASH_CHECKPOINT_KEY)).toBe(sha256(newContent.trim()));
254
- expect(store.get(HASH_CHECKPOINT_KEY)).not.toBe(sha256(oldContent));
255
- });
256
-
257
- test("file present but readFileSync throws — checkpoint UNCHANGED; warn logged", async () => {
258
- const content = "## Release U\n\nSimulated read failure.\n";
259
- writeFileSync(workspacePath, content, "utf-8");
260
-
261
- readFileSyncOverride = (path) => {
262
- if (typeof path === "string" && path === workspacePath) {
263
- throw new Error("EACCES simulated");
264
- }
265
- return undefined;
266
- };
267
-
268
- try {
269
- await runUpdateBulletinJobIfNeeded();
270
- } finally {
271
- readFileSyncOverride = null;
272
- }
273
-
274
- expect(runBackgroundJobCalls).toBe(0);
275
- expect(store.has(HASH_CHECKPOINT_KEY)).toBe(false);
276
- expect(setCheckpointCallCount).toBe(0);
277
- });
278
-
279
- test("runBackgroundJob throws — function does not reject; warning logged", async () => {
280
- const content = "## Release Z";
281
- writeFileSync(workspacePath, content, "utf-8");
282
- runBackgroundJobShouldThrow = true;
283
-
284
- // Must not throw.
285
- await expect(runUpdateBulletinJobIfNeeded()).resolves.toBeUndefined();
286
-
287
- expect(runBackgroundJobCalls).toBe(1);
288
- // Hash was never updated because the try/catch returned before the
289
- // self-healing step.
290
- expect(store.has(HASH_CHECKPOINT_KEY)).toBe(false);
291
- });
292
- });
@@ -1,14 +0,0 @@
1
- import { z } from "zod";
2
-
3
- export const UpdatesConfigSchema = z
4
- .object({
5
- enabled: z
6
- .boolean({ error: "updates.enabled must be a boolean" })
7
- .default(true)
8
- .describe(
9
- "Whether to dispatch a background conversation when <workspace>/UPDATES.md has unprocessed content. When false, release-update bulletins are written by migrations but never processed by the agent.",
10
- ),
11
- })
12
- .describe("Release update bulletin configuration");
13
-
14
- export type UpdatesConfig = z.infer<typeof UpdatesConfigSchema>;
@@ -1,227 +0,0 @@
1
- /**
2
- * Tests for the ClickHouse compaction log writer.
3
- *
4
- * The writer is exercised with an injected `fetchImpl` so the tests pin the
5
- * wire shape: lazy CREATE TABLE on first write, INSERT statements via the
6
- * `query` URL param with JSONEachRow bodies, and the start/end row mappings
7
- * (sentinels on the start row, full `ContextWindowResult` projection on the
8
- * end row).
9
- */
10
- import { describe, expect, test } from "bun:test";
11
-
12
- import type { ContextWindowResult } from "../../plugins/defaults/compaction/window-manager.js";
13
- import {
14
- ClickHouseCompactionLogWriter,
15
- type CompactionEndEvent,
16
- type CompactionStartEvent,
17
- } from "../compaction-log-writer-clickhouse.js";
18
-
19
- interface CapturedRequest {
20
- url: URL;
21
- body: string;
22
- }
23
-
24
- function createWriter(overrides?: {
25
- fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
26
- }): { writer: ClickHouseCompactionLogWriter; requests: CapturedRequest[] } {
27
- const requests: CapturedRequest[] = [];
28
- const fetchImpl = (overrides?.fetchImpl ??
29
- (async (url: string, init?: RequestInit) => {
30
- requests.push({ url: new URL(url), body: String(init?.body ?? "") });
31
- return new Response("", { status: 200 });
32
- })) as typeof fetch;
33
- const writer = new ClickHouseCompactionLogWriter(
34
- { database: "analytics", table: "compaction_logs", user: "writer" },
35
- {
36
- resolveUrl: async () => "http://clickhouse.example.com:8123",
37
- resolvePassword: async () => "secret",
38
- resolveAssistantId: async () => "assistant-123",
39
- fetchImpl,
40
- },
41
- );
42
- return { writer, requests };
43
- }
44
-
45
- const startEvent: CompactionStartEvent = {
46
- type: "context_compacting",
47
- compactionId: "comp-1",
48
- requestId: "req-1",
49
- trigger: "budget",
50
- startedAt: 1000,
51
- messages: [
52
- { role: "user", content: [{ type: "text", text: "hi" }] },
53
- { role: "assistant", content: [{ type: "text", text: "hello" }] },
54
- ],
55
- };
56
-
57
- const result: ContextWindowResult = {
58
- messages: [],
59
- compacted: true,
60
- previousEstimatedInputTokens: 900,
61
- estimatedInputTokens: 300,
62
- maxInputTokens: 1000,
63
- thresholdTokens: 850,
64
- compactedMessages: 10,
65
- compactedPersistedMessages: 8,
66
- preservedTailMessages: 2,
67
- summaryCalls: 1,
68
- summaryInputTokens: 880,
69
- summaryOutputTokens: 120,
70
- summaryModel: "test-model",
71
- summaryText: "summary text",
72
- reason: "auto",
73
- exhausted: false,
74
- };
75
-
76
- const endEvent: CompactionEndEvent = {
77
- type: "compaction_completed",
78
- compactionId: "comp-1",
79
- requestId: "req-1",
80
- trigger: "budget",
81
- startedAt: 1000,
82
- finishedAt: 1500,
83
- result,
84
- messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
85
- };
86
-
87
- describe("ClickHouseCompactionLogWriter", () => {
88
- test("first write creates the table then inserts", async () => {
89
- const { writer, requests } = createWriter();
90
-
91
- await writer.writeStart("conv-1", startEvent);
92
-
93
- expect(requests.length).toBe(2);
94
- expect(requests[0]!.body).toContain("CREATE TABLE IF NOT EXISTS");
95
- expect(requests[0]!.body).toContain("`compaction_logs`");
96
- expect(requests[0]!.url.searchParams.get("database")).toBe("analytics");
97
-
98
- const insert = requests[1]!;
99
- expect(insert.url.searchParams.get("query")).toBe(
100
- "INSERT INTO `compaction_logs` FORMAT JSONEachRow",
101
- );
102
- const row = JSON.parse(insert.body) as Record<string, unknown>;
103
- expect(row.assistant_id).toBe("assistant-123");
104
- expect(row.conversation_id).toBe("conv-1");
105
- expect(row.compaction_id).toBe("comp-1");
106
- expect(row.request_id).toBe("req-1");
107
- expect(row.phase).toBe("start");
108
- expect(row.trigger).toBe("budget");
109
- expect(row.started_at).toBe(1000);
110
- expect(row.pre_message_count).toBe(2);
111
- // End-only fields stay at their sentinels on the start row.
112
- expect(row.finished_at).toBe(0);
113
- expect(row.duration_ms).toBe(-1);
114
- expect(row.estimated_input_tokens).toBe(-1);
115
- expect(row.summary_model).toBe("");
116
- });
117
-
118
- test("table creation happens once across writes", async () => {
119
- const { writer, requests } = createWriter();
120
-
121
- await writer.writeStart("conv-1", startEvent);
122
- await writer.writeEnd("conv-1", endEvent);
123
-
124
- const ddl = requests.filter((r) => r.body.includes("CREATE TABLE"));
125
- expect(ddl.length).toBe(1);
126
- expect(requests.length).toBe(3);
127
- });
128
-
129
- test("end row projects the full ContextWindowResult", async () => {
130
- const { writer, requests } = createWriter();
131
-
132
- await writer.writeEnd("conv-1", endEvent);
133
-
134
- const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
135
- expect(row.phase).toBe("end");
136
- expect(row.finished_at).toBe(1500);
137
- expect(row.duration_ms).toBe(500);
138
- expect(row.basis_message_count).toBe(1);
139
- expect(row.compacted).toBe(1);
140
- expect(row.previous_estimated_input_tokens).toBe(900);
141
- expect(row.estimated_input_tokens).toBe(300);
142
- expect(row.max_input_tokens).toBe(1000);
143
- expect(row.threshold_tokens).toBe(850);
144
- expect(row.compacted_messages).toBe(10);
145
- expect(row.compacted_persisted_messages).toBe(8);
146
- expect(row.preserved_tail_messages).toBe(2);
147
- expect(row.summary_calls).toBe(1);
148
- expect(row.summary_input_tokens).toBe(880);
149
- expect(row.summary_output_tokens).toBe(120);
150
- expect(row.summary_model).toBe("test-model");
151
- expect(row.summary_failed).toBe(-1);
152
- expect(row.reason).toBe("auto");
153
- expect(row.exhausted).toBe(0);
154
- expect(row.summary_text).toBe("summary text");
155
- });
156
-
157
- test("end row maps explicit summaryFailed and omitted optionals", async () => {
158
- const { writer, requests } = createWriter();
159
-
160
- await writer.writeEnd("conv-1", {
161
- ...endEvent,
162
- result: {
163
- ...result,
164
- compacted: false,
165
- summaryFailed: true,
166
- preservedTailMessages: undefined,
167
- reason: undefined,
168
- exhausted: true,
169
- },
170
- });
171
-
172
- const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
173
- expect(row.compacted).toBe(0);
174
- expect(row.summary_failed).toBe(1);
175
- expect(row.preserved_tail_messages).toBe(-1);
176
- expect(row.reason).toBe("");
177
- expect(row.exhausted).toBe(1);
178
- });
179
-
180
- test("failed table creation retries on the next write", async () => {
181
- let calls = 0;
182
- const requests: CapturedRequest[] = [];
183
- const { writer } = createWriter({
184
- fetchImpl: async (url: string, init?: RequestInit) => {
185
- calls++;
186
- requests.push({ url: new URL(url), body: String(init?.body ?? "") });
187
- if (calls === 1) return new Response("boom", { status: 500 });
188
- return new Response("", { status: 200 });
189
- },
190
- });
191
-
192
- await expect(writer.writeStart("conv-1", startEvent)).rejects.toThrow(
193
- "ClickHouse compaction-log write failed",
194
- );
195
-
196
- await writer.writeStart("conv-1", startEvent);
197
- const ddl = requests.filter((r) => r.body.includes("CREATE TABLE"));
198
- expect(ddl.length).toBe(2);
199
- });
200
-
201
- test("write failures surface the HTTP status and body", async () => {
202
- let calls = 0;
203
- const { writer } = createWriter({
204
- fetchImpl: async () => {
205
- calls++;
206
- if (calls === 1) return new Response("", { status: 200 });
207
- return new Response("table is read-only", { status: 403 });
208
- },
209
- });
210
-
211
- await expect(writer.writeEnd("conv-1", endEvent)).rejects.toThrow(
212
- /HTTP 403.*table is read-only/,
213
- );
214
- });
215
-
216
- test("long summary text is truncated", async () => {
217
- const { writer, requests } = createWriter();
218
-
219
- await writer.writeEnd("conv-1", {
220
- ...endEvent,
221
- result: { ...result, summaryText: "x".repeat(10_000) },
222
- });
223
-
224
- const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
225
- expect((row.summary_text as string).length).toBe(4000);
226
- });
227
- });
@@ -1,78 +0,0 @@
1
- /**
2
- * Cadence logic for conversation starters generation.
3
- *
4
- * Decides whether a new generation job should be enqueued based on how many
5
- * active memory items have accumulated since the last generation.
6
- */
7
-
8
- import { and, eq, inArray, sql } from "drizzle-orm";
9
-
10
- import { getLogger } from "../util/logger.js";
11
- import { getDb } from "./db-connection.js";
12
- import { enqueueMemoryJob, isMemoryEnabled } from "./jobs-store.js";
13
- import { rawGet } from "./raw-query.js";
14
- import { memoryCheckpoints, memoryJobs } from "./schema.js";
15
-
16
- const log = getLogger("conversation-starters-cadence");
17
-
18
- /**
19
- * Check whether enough new memory items have accumulated to justify
20
- * generating a fresh batch of conversation starters.
21
- */
22
- export function maybeEnqueueConversationStartersJob(scopeId: string): void {
23
- if (!isMemoryEnabled()) return;
24
-
25
- const db = getDb();
26
-
27
- // Count total active memory items
28
- const countRow = rawGet<{ c: number }>(
29
- `SELECT COUNT(*) AS c FROM memory_graph_nodes WHERE fidelity != 'gone' AND scope_id = ?`,
30
- scopeId,
31
- );
32
- const totalActive = countRow?.c ?? 0;
33
- if (totalActive === 0) return;
34
-
35
- // Read checkpoint: item count at last generation (scoped so each scope tracks independently)
36
- const checkpointKey = `conversation_starters:item_count_at_last_gen:${scopeId}`;
37
- const checkpoint = db
38
- .select({ value: memoryCheckpoints.value })
39
- .from(memoryCheckpoints)
40
- .where(eq(memoryCheckpoints.key, checkpointKey))
41
- .get();
42
- const parsedLastCount = checkpoint ? parseInt(checkpoint.value, 10) : 0;
43
- const lastCount = Number.isFinite(parsedLastCount) ? parsedLastCount : 0;
44
-
45
- // Cadence formula
46
- let threshold: number;
47
- if (totalActive <= 10) {
48
- threshold = 1;
49
- } else if (totalActive <= 50) {
50
- threshold = 5;
51
- } else {
52
- threshold = 10;
53
- }
54
-
55
- const checkpointAhead = totalActive < lastCount;
56
- const delta = Math.max(0, totalActive - lastCount);
57
- if (!checkpointAhead && delta < threshold) return;
58
-
59
- // Dedup: don't enqueue if a pending/running job for this scope already exists
60
- const existing = db
61
- .select({ id: memoryJobs.id })
62
- .from(memoryJobs)
63
- .where(
64
- and(
65
- eq(memoryJobs.type, "generate_conversation_starters"),
66
- inArray(memoryJobs.status, ["pending", "running"]),
67
- sql`json_extract(${memoryJobs.payload}, '$.scopeId') = ${scopeId}`,
68
- ),
69
- )
70
- .get();
71
- if (existing) return;
72
-
73
- enqueueMemoryJob("generate_conversation_starters", { scopeId });
74
- log.info(
75
- { totalActive, lastCount, delta, threshold, scopeId, checkpointAhead },
76
- "Enqueued conversation starters generation job",
77
- );
78
- }