@vellumai/assistant 0.8.11 → 0.8.12-staging.1

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 (219) 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 +126 -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-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -1,37 +1,29 @@
1
- import { readdirSync, readFileSync } from "node:fs";
1
+ import { readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { describe, expect, test } from "bun:test";
4
4
 
5
- type Registry = {
6
- flags: Array<{
7
- key: string;
8
- scope: string;
9
- defaultEnabled: boolean;
10
- }>;
11
- };
12
-
13
- const LEGACY_FLAGGED_RELEASE_NOTE_ALLOWLIST = new Map<string, string>([
14
- [
15
- "045-release-notes-meet-avatar.ts",
16
- "Historical bulletin that already shipped before this guard existed.",
17
- ],
18
- ]);
19
-
20
- const FLAGGED_FEATURE_LANGUAGE_PATTERNS: Array<{
21
- label: string;
22
- pattern: RegExp;
23
- }> = [
24
- { label: "feature flag", pattern: /\bfeature[- ]flag(?:ged)?\b/i },
25
- { label: "rollout flag", pattern: /\brollout flag\b/i },
26
- { label: "behind ... flag", pattern: /\bbehind\b[^\n.]{0,120}\bflag\b/i },
27
- { label: "gated on", pattern: /\bgated on\b/i },
28
- { label: "when enabled", pattern: /\bwhen enabled\b/i },
5
+ /**
6
+ * The historical release-note workspace migrations. These appended bulletins
7
+ * to `<workspace>/UPDATES.md`, which a background job processed at daemon
8
+ * startup. That job has been removed — nothing consumes UPDATES.md release
9
+ * notes anymore — so the set below is frozen. Migration files themselves are
10
+ * append-only and must never be deleted (see the migrations AGENTS.md), but
11
+ * no new ones of this shape may be added.
12
+ */
13
+ const FROZEN_RELEASE_NOTE_MIGRATIONS = [
14
+ "043-release-notes-latex-rendering.ts",
15
+ "045-release-notes-meet-avatar.ts",
16
+ "049-release-notes-default-sonnet.ts",
17
+ "053-release-notes-acp-codex.ts",
18
+ "055-release-notes-agentic-recall.ts",
19
+ "056-release-notes-inference-profile-reordering.ts",
20
+ "058-release-notes-acp-sessions-ui.ts",
21
+ "063-release-notes-dynamic-model-context.ts",
22
+ "067-release-notes-safe-storage-limits.ts",
23
+ "068-release-notes-local-timezone.ts",
24
+ "078-release-notes-tavily-web-search.ts",
29
25
  ];
30
26
 
31
- function getRepoRoot(): string {
32
- return join(process.cwd(), "..");
33
- }
34
-
35
27
  function getReleaseNoteMigrationFiles(): string[] {
36
28
  const migrationsDir = join(process.cwd(), "src", "workspace", "migrations");
37
29
  return readdirSync(migrationsDir)
@@ -39,77 +31,35 @@ function getReleaseNoteMigrationFiles(): string[] {
39
31
  .sort();
40
32
  }
41
33
 
42
- function loadDefaultDisabledAssistantFlagKeys(): string[] {
43
- const registryPath = join(
44
- getRepoRoot(),
45
- "meta",
46
- "feature-flags",
47
- "feature-flag-registry.json",
48
- );
49
- const registry = JSON.parse(readFileSync(registryPath, "utf-8")) as Registry;
50
- return registry.flags
51
- .filter((flag) => flag.scope === "assistant" && !flag.defaultEnabled)
52
- .map((flag) => flag.key)
53
- .sort();
54
- }
55
-
56
- function escapeRegExp(value: string): string {
57
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58
- }
59
-
60
- function featureFlagKeyPattern(key: string): RegExp {
61
- const escaped = escapeRegExp(key);
62
- if (key.includes("-")) {
63
- return new RegExp(`(?:^|[^a-z0-9-])${escaped}(?:$|[^a-z0-9-])`, "i");
64
- }
65
-
66
- return new RegExp("[`'\"]" + escaped + "[`'\"]", "i");
67
- }
34
+ describe("workspace release-note migrations guard", () => {
35
+ test("no new release-note migrations are added", () => {
36
+ const found = getReleaseNoteMigrationFiles();
37
+ const newMigrations = found.filter(
38
+ (fileName) => !FROZEN_RELEASE_NOTE_MIGRATIONS.includes(fileName),
39
+ );
68
40
 
69
- describe("workspace release-note migrations feature flag guard", () => {
70
- test("new release-note migrations do not announce default-disabled feature-flagged work", () => {
71
- const migrationsDir = join(process.cwd(), "src", "workspace", "migrations");
72
- const defaultDisabledFlagKeys = loadDefaultDisabledAssistantFlagKeys();
73
- const violations: string[] = [];
74
-
75
- for (const fileName of getReleaseNoteMigrationFiles()) {
76
- if (LEGACY_FLAGGED_RELEASE_NOTE_ALLOWLIST.has(fileName)) {
77
- continue;
78
- }
79
-
80
- const content = readFileSync(join(migrationsDir, fileName), "utf-8");
81
-
82
- for (const { label, pattern } of FLAGGED_FEATURE_LANGUAGE_PATTERNS) {
83
- if (pattern.test(content)) {
84
- violations.push(
85
- `${fileName}: contains flagged-feature language "${label}"`,
86
- );
87
- }
88
- }
89
-
90
- for (const key of defaultDisabledFlagKeys) {
91
- if (featureFlagKeyPattern(key).test(content)) {
92
- violations.push(
93
- `${fileName}: references default-disabled assistant feature flag "${key}"`,
94
- );
95
- }
96
- }
97
- }
98
-
99
- if (violations.length > 0) {
41
+ if (newMigrations.length > 0) {
100
42
  const message = [
101
- "Release-note migrations write to UPDATES.md, which is processed without checking feature flags.",
102
- "Do not announce features that are still behind default-disabled assistant flags or rollout flags.",
103
- "Wait until GA and add a new append-only release-note migration with a new marker.",
104
- "",
105
- "Violations:",
106
- ...violations.map((violation) => ` - ${violation}`),
43
+ "Release-note migrations append to <workspace>/UPDATES.md, but the",
44
+ "background job that processed that file has been removed — nothing",
45
+ "consumes release notes written there. Do not add new release-note",
46
+ "migrations until a replacement surfacing mechanism is designed.",
107
47
  "",
108
- "If this is an already-shipped historical bulletin, add a narrow entry to",
109
- "LEGACY_FLAGGED_RELEASE_NOTE_ALLOWLIST in workspace-release-notes-feature-flag-guard.test.ts.",
48
+ "New release-note migrations found:",
49
+ ...newMigrations.map((fileName) => ` - ${fileName}`),
110
50
  ].join("\n");
111
51
 
112
- expect(violations, message).toEqual([]);
52
+ expect(newMigrations, message).toEqual([]);
53
+ }
54
+ });
55
+
56
+ test("frozen release-note migrations are never deleted", () => {
57
+ // Migrations are append-only: even though their bulletins are no longer
58
+ // processed, deleting an entry breaks the sequential migration chain on
59
+ // existing installs.
60
+ const found = getReleaseNoteMigrationFiles();
61
+ for (const fileName of FROZEN_RELEASE_NOTE_MIGRATIONS) {
62
+ expect(found).toContain(fileName);
113
63
  }
114
64
  });
115
65
  });
package/src/agent/loop.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  estimateToolsTokens,
12
12
  getCalibrationProviderKey,
13
13
  } from "../context/token-estimator.js";
14
+ import { spoolAndStubOversizedToolResults } from "../context/tool-result-spool.js";
14
15
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
15
16
  import { parseActualTokensFromError } from "../daemon/parse-actual-tokens-from-error.js";
16
17
  import type { TrustContext } from "../daemon/trust-context.js";
@@ -298,25 +299,27 @@ export type AgentEvent =
298
299
  /** The running history before injection stripping and compaction. */
299
300
  messages: Message[];
300
301
  }
301
- | {
302
+ | ({
302
303
  /**
303
304
  * Emitted after the loop's inline mid-loop compaction pipeline runs,
304
305
  * immediately before re-injection — whether or not the pipeline actually
305
- * compacted. The daemon's event dispatcher always commits `messages` (the
306
- * stripped pre-compaction history) as the conversation's durable message
307
- * state, so re-injection (the post-compaction hook) re-applies
308
- * injections onto the stripped base rather than stacking on top of the
309
- * still-injected messages. When `result.compacted` is set it
310
- * additionally commits the durable compaction result (DB-record fields,
311
- * graph-memory side effects, SSE) and flips the per-turn re-injection
312
- * guards on the handler state.
306
+ * compacted. Carries the pipeline's `ContextWindowResult` unnested into
307
+ * the event, so `messages` here is the pipeline's output history. The
308
+ * pre-compaction history lives on the paired `context_compacting` start
309
+ * event (correlated by `compactionId`); consumers that need the
310
+ * stripped pre-compaction base re-derive it from the start event via
311
+ * `stripInjectionsForCompaction`.
312
+ *
313
+ * The daemon's event dispatcher commits the stripped pre-compaction
314
+ * base as the conversation's durable message state, so re-injection
315
+ * (the post-compaction hook) re-applies injections onto the stripped
316
+ * base rather than stacking on top of the still-injected messages.
317
+ * When `compacted` is set it additionally commits the durable
318
+ * compaction result (DB-record fields, graph-memory side effects, SSE)
319
+ * and projects Slack provenance from the pre-compaction base.
313
320
  *
314
321
  * Treated as a critical event: a failed durable commit re-throws so the
315
322
  * turn aborts rather than re-injecting against half-applied state.
316
- *
317
- * `messages` is the stripped pre-compaction history the summary was
318
- * built from; the dispatcher uses it to project Slack provenance onto
319
- * the compacted result.
320
323
  */
321
324
  type: "compaction_completed";
322
325
  /** Correlates this end event with its `context_compacting` pair. */
@@ -330,9 +333,7 @@ export type AgentEvent =
330
333
  startedAt: number;
331
334
  /** Epoch ms when the compaction pipeline returned. */
332
335
  finishedAt: number;
333
- result: ContextWindowResult;
334
- messages: Message[];
335
- }
336
+ } & ContextWindowResult)
336
337
  | {
337
338
  /**
338
339
  * Emitted right after the loop strips runtime injections from the
@@ -595,6 +596,16 @@ export interface AgentLoopConstructorOptions {
595
596
  * post-compaction re-injection resolve the live conversation through.
596
597
  */
597
598
  conversationId: string;
599
+ /**
600
+ * Resolve the conversation's on-disk directory, used to spool oversized
601
+ * tool results to `.tool-results/` and swap the inline copy for the
602
+ * post-turn pass's stub at result time — before the result joins history,
603
+ * so the provider-bound prefix stays append-only for prompt caching.
604
+ * Returns `null` while the directory cannot be resolved (e.g. the
605
+ * conversation row is not yet persisted); the loop then skips the
606
+ * result-time pass and the post-turn truncation covers the turn instead.
607
+ */
608
+ resolveConversationDir?: () => string | null;
598
609
  }
599
610
 
600
611
  export class AgentLoop {
@@ -616,6 +627,9 @@ export class AgentLoop {
616
627
  */
617
628
  private readonly conversationId: string;
618
629
 
630
+ /** See {@link AgentLoopConstructorOptions.resolveConversationDir}. */
631
+ private readonly resolveConversationDir: (() => string | null) | null;
632
+
619
633
  /**
620
634
  * Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
621
635
  * conversation, so it is the source of truth for the cross-turn failure
@@ -635,6 +649,7 @@ export class AgentLoop {
635
649
  resolveTools,
636
650
  resolveSystemPrompt,
637
651
  conversationId,
652
+ resolveConversationDir,
638
653
  } = options;
639
654
  this.provider = provider;
640
655
  this.systemPrompt = systemPrompt;
@@ -644,6 +659,7 @@ export class AgentLoop {
644
659
  this.resolveSystemPrompt = resolveSystemPrompt ?? null;
645
660
  this.toolExecutor = toolExecutor ?? null;
646
661
  this.conversationId = conversationId;
662
+ this.resolveConversationDir = resolveConversationDir ?? null;
647
663
  this.compactionCircuit = new CompactionCircuit(this.conversationId);
648
664
  }
649
665
 
@@ -782,10 +798,10 @@ export class AgentLoop {
782
798
  onEvent,
783
799
  );
784
800
  }
785
- // Emit unconditionally: the dispatcher commits the stripped `messages` as the
786
- // durable message base whether or not the pipeline compacted (re-injection
787
- // reads it), and runs the durable compaction commit only when
788
- // `result.compacted`.
801
+ // Emit unconditionally: the dispatcher commits the stripped pre-compaction
802
+ // base (re-derived from the start event) as the durable message base
803
+ // whether or not the pipeline compacted (re-injection reads it), and runs
804
+ // the durable compaction commit only when `compacted`.
789
805
  await onEvent({
790
806
  type: "compaction_completed",
791
807
  compactionId,
@@ -793,8 +809,7 @@ export class AgentLoop {
793
809
  trigger,
794
810
  startedAt,
795
811
  finishedAt: Date.now(),
796
- result: compactResult,
797
- messages: rawHistory,
812
+ ...compactResult,
798
813
  });
799
814
  const exhausted = compactResult.exhausted ?? false;
800
815
  const autoCompressApplied = compactResult.autoCompressApplied ?? false;
@@ -886,6 +901,19 @@ export class AgentLoop {
886
901
  let overflowAutoCompressApplied = false;
887
902
  const rlog = log.child({ requestId });
888
903
 
904
+ // Conversation directory for the result-time tool-result spool/stub pass.
905
+ // Resolved once per run; `null` disables the pass for this run and the
906
+ // post-turn truncation covers the turn instead.
907
+ let conversationDir: string | null = null;
908
+ try {
909
+ conversationDir = this.resolveConversationDir?.() ?? null;
910
+ } catch (err) {
911
+ rlog.warn(
912
+ { err },
913
+ "Resolving conversation dir for tool-result spooling failed (non-fatal)",
914
+ );
915
+ }
916
+
889
917
  // Resolve the inference-profile override that applies right now. The
890
918
  // optional resolver lets a turn observe a confirmed mid-turn profile switch
891
919
  // before the next model call; absent a resolver the turn-start value holds.
@@ -1724,11 +1752,39 @@ export class AgentLoop {
1724
1752
  }),
1725
1753
  );
1726
1754
 
1755
+ // Spool oversized results to `.tool-results/` and swap the inline
1756
+ // copy for the post-turn pass's stub now — on the raw blocks, before
1757
+ // the post-tool-use hooks, event emission, and history append. Running
1758
+ // ahead of the hooks means the spooled file holds the tool's full
1759
+ // output rather than the truncate plugin's tail-dropped copy, and the
1760
+ // hooks then see the stub. Stubbing before the first send keeps the
1761
+ // provider-bound history strictly append-only (rewriting an earlier
1762
+ // message between calls would invalidate the prompt-cache prefix on
1763
+ // every iteration).
1764
+ if (conversationDir) {
1765
+ const toolNameByUseId = new Map(
1766
+ toolUseBlocks.map((tu) => [tu.id, tu.name]),
1767
+ );
1768
+ try {
1769
+ spoolAndStubOversizedToolResults(rawResultBlocks, {
1770
+ conversationDir,
1771
+ toolNameById: (id) => toolNameByUseId.get(id),
1772
+ });
1773
+ } catch (err) {
1774
+ rlog.warn(
1775
+ { err, turn: toolUseTurns },
1776
+ "Spooling oversized tool results to disk failed (non-fatal)",
1777
+ );
1778
+ }
1779
+ }
1780
+
1727
1781
  // Run the `post-tool-use` hook once per tool result, after the tool
1728
1782
  // returns and before the result joins the provider-bound history.
1729
1783
  // The default tool-result-truncate plugin tail-drops oversized output
1730
- // to fit the context window; user hooks can swap in a smarter strategy
1731
- // (e.g. a summariser) or observe results for side effects.
1784
+ // to fit the context window (spool-stubbed results are already tiny;
1785
+ // spool-exempt ones still rely on it); user hooks can swap in a
1786
+ // smarter strategy (e.g. a summariser) or observe results for side
1787
+ // effects.
1732
1788
  const contextWindowTokens =
1733
1789
  resolveContextWindow?.().maxInputTokens ??
1734
1790
  this.config.maxInputTokens ??
@@ -1746,6 +1802,7 @@ export class AgentLoop {
1746
1802
  toolResponse: block as ToolResultContent,
1747
1803
  messages: history,
1748
1804
  additionalContext: null,
1805
+ model: response.model,
1749
1806
  maxInputTokens: contextWindowTokens,
1750
1807
  logger: rlog,
1751
1808
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `usage_progress` SSE event.
3
+ *
4
+ * Emitted after each LLM call with per-call token deltas and estimated
5
+ * cost. Clients accumulate these additively for live-updating usage
6
+ * metrics (e.g. inline subagent token counters). A UI-only hint — it
7
+ * does not persist to the DB or affect billing.
8
+ *
9
+ * Unlike `usage_update`, which carries the conversation's running
10
+ * totals, this event carries only the deltas for a single LLM call.
11
+ *
12
+ * Canonical wire-contract source. Daemon code imports the type
13
+ * directly from this file; external consumers import via
14
+ * `@vellumai/assistant-api`.
15
+ */
16
+
17
+ import { z } from "zod";
18
+
19
+ export const UsageProgressEventSchema = z.object({
20
+ type: z.literal("usage_progress"),
21
+ conversationId: z.string(),
22
+ inputTokens: z.number(),
23
+ outputTokens: z.number(),
24
+ estimatedCost: z.number(),
25
+ model: z.string(),
26
+ });
27
+
28
+ export type UsageProgressEvent = z.infer<typeof UsageProgressEventSchema>;
package/src/api/index.ts CHANGED
@@ -49,6 +49,7 @@ import { UISurfaceCompleteEventSchema } from "./events/ui-surface-complete.js";
49
49
  import { UISurfaceDismissEventSchema } from "./events/ui-surface-dismiss.js";
50
50
  import { UISurfaceShowEventSchema } from "./events/ui-surface-show.js";
51
51
  import { UISurfaceUpdateEventSchema } from "./events/ui-surface-update.js";
52
+ import { UsageProgressEventSchema } from "./events/usage-progress.js";
52
53
  import { UsageUpdateEventSchema } from "./events/usage-update.js";
53
54
  import { UserMessageEchoEventSchema } from "./events/user-message-echo.js";
54
55
 
@@ -310,6 +311,10 @@ export {
310
311
  type UISurfaceUpdateEvent,
311
312
  UISurfaceUpdateEventSchema,
312
313
  } from "./events/ui-surface-update.js";
314
+ export {
315
+ type UsageProgressEvent,
316
+ UsageProgressEventSchema,
317
+ } from "./events/usage-progress.js";
313
318
  export {
314
319
  type UsageUpdateEvent,
315
320
  UsageUpdateEventSchema,
@@ -497,6 +502,7 @@ export const AssistantEventSchema = z.discriminatedUnion("type", [
497
502
  UISurfaceDismissEventSchema,
498
503
  UISurfaceShowEventSchema,
499
504
  UISurfaceUpdateEventSchema,
505
+ UsageProgressEventSchema,
500
506
  UsageUpdateEventSchema,
501
507
  UserMessageEchoEventSchema,
502
508
  ]);
@@ -52,6 +52,8 @@ mock.module("../util/platform.js", () => ({
52
52
  getWorkspaceRoutesDir: () => workspacePath("routes"),
53
53
  vellumRoot: () => testWorkspaceDir,
54
54
  getDataDir: () => workspacePath("data"),
55
+ getConfigQuarantineNoticePath: () =>
56
+ workspacePath("data", "config-quarantine-notice.json"),
55
57
  getDbPath: () => workspacePath("data", "db", "assistant.db"),
56
58
  ensureDataDir: () => {
57
59
  mkdirSync(workspacePath("data", "db"), { recursive: true });
@@ -6,12 +6,11 @@
6
6
  */
7
7
 
8
8
  import { createHash } from "node:crypto";
9
- import { createWriteStream, existsSync, readFileSync } from "node:fs";
9
+ import { existsSync, readFileSync } from "node:fs";
10
10
  import { readFile, stat, writeFile } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
 
14
- import archiver from "archiver";
15
14
  import JSZip from "jszip";
16
15
 
17
16
  import { getApp, getAppDirPath, isMultifileApp } from "../memory/app-store.js";
@@ -37,6 +36,14 @@ export interface BundleResult {
37
36
  iconImageBase64?: string;
38
37
  }
39
38
 
39
+ function generateZipBuffer(zip: JSZip): Promise<Buffer> {
40
+ return zip.generateAsync({
41
+ type: "nodebuffer",
42
+ compression: "DEFLATE",
43
+ compressionOptions: { level: 9 },
44
+ });
45
+ }
46
+
40
47
  function isDefaultMainScaffold(source: string): boolean {
41
48
  const normalized = source.replace(/\s+/g, " ").trim();
42
49
  return (
@@ -177,54 +184,31 @@ export async function packageApp(
177
184
  const bundleFilename = `${safeName}-${uniqueSuffix}.vellum`;
178
185
  const bundlePath = join(tmpdir(), bundleFilename);
179
186
 
180
- await new Promise<void>((resolve, reject) => {
181
- const output = createWriteStream(bundlePath);
182
- const archive = archiver("zip", { zlib: { level: 9 } });
183
-
184
- output.on("close", () => resolve());
185
- output.on("error", (err: Error) => reject(err));
186
- archive.on("error", (err: Error) => reject(err));
187
- archive.on("warning", (err: Error) => {
188
- // Only reject on fatal warnings
189
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
190
- reject(err);
191
- }
192
- });
193
-
194
- archive.pipe(output);
187
+ const zip = new JSZip();
195
188
 
196
- // Add manifest.json at root level
197
- archive.append(serializeManifest(manifest), { name: "manifest.json" });
189
+ // Add manifest.json at root level
190
+ zip.file("manifest.json", serializeManifest(manifest));
198
191
 
199
- // Add compiled dist/ files
200
- for (const file of compiledFiles) {
201
- archive.append(file.data, { name: file.name });
202
- }
192
+ // Add compiled dist/ files
193
+ for (const file of compiledFiles) {
194
+ zip.file(file.name, file.data);
195
+ }
203
196
 
204
- // Include app icon if one was generated
205
- const iconPath = join(getAppDirPath(appId), "icon.png");
206
- if (existsSync(iconPath)) {
207
- archive.append(readFileSync(iconPath), { name: "icon.png" });
208
- }
197
+ // Include app icon if one was generated
198
+ const iconPath = join(appDir, "icon.png");
199
+ if (existsSync(iconPath)) {
200
+ zip.file("icon.png", readFileSync(iconPath));
201
+ }
209
202
 
210
- archive.finalize();
211
- });
203
+ await writeFile(bundlePath, await generateZipBuffer(zip));
212
204
 
213
205
  // Sign the bundle if a signing callback is provided
214
206
  if (requestSignature) {
215
207
  try {
216
208
  const signatureJson = await signBundle(bundlePath, requestSignature);
217
209
 
218
- // Re-open the zip and add signature.json
219
- const zipBuffer = await readFile(bundlePath);
220
- const zip = await JSZip.loadAsync(zipBuffer);
221
210
  zip.file("signature.json", JSON.stringify(signatureJson, null, 2));
222
- const signedBuffer = await zip.generateAsync({
223
- type: "nodebuffer",
224
- compression: "DEFLATE",
225
- compressionOptions: { level: 9 },
226
- });
227
- await writeFile(bundlePath, signedBuffer);
211
+ await writeFile(bundlePath, await generateZipBuffer(zip));
228
212
 
229
213
  bundlerLog.info({ appId }, "Bundle signed successfully");
230
214
  } catch (err) {
@@ -250,9 +234,8 @@ export async function packageApp(
250
234
 
251
235
  // Read icon for inclusion in the response
252
236
  let iconImageBase64: string | undefined;
253
- const iconFilePath = join(getAppDirPath(appId), "icon.png");
254
- if (existsSync(iconFilePath)) {
255
- iconImageBase64 = readFileSync(iconFilePath).toString("base64");
237
+ if (existsSync(iconPath)) {
238
+ iconImageBase64 = readFileSync(iconPath).toString("base64");
256
239
  }
257
240
 
258
241
  return { bundlePath, manifest, iconImageBase64 };
@@ -1312,7 +1312,7 @@ export class CallController {
1312
1312
 
1313
1313
  // Mark the linked guardian action request as timed out and
1314
1314
  // send expiry notices to guardian destinations. Deliveries
1315
- // must be captured before markTimedOutWithReason changes
1315
+ // must be captured before expiring the request changes
1316
1316
  // their status.
1317
1317
  const pendingActionRequest = getPendingCanonicalRequestByCallSessionId(
1318
1318
  this.callSessionId,