@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -148,7 +148,7 @@ mock.module(
148
148
  // agent loop's compaction path consumes, mirroring the real manager's
149
149
  // `overflowStepToResult` so the loop sees the rung's reduced history, injection
150
150
  // mode, terminal auto-compress flag, and exhaustion.
151
- function makeOverflowLadderStub(): {
151
+ function makeOverflowLadderStub(stubOpts: { maxInputTokens?: number } = {}): {
152
152
  resetOverflowRecovery: () => void;
153
153
  reduceOverflowOneRung: (
154
154
  msgs: Message[],
@@ -161,6 +161,7 @@ function makeOverflowLadderStub(): {
161
161
  signal?: AbortSignal,
162
162
  ) => Promise<unknown>;
163
163
  } {
164
+ const maxInputTokens = stubOpts.maxInputTokens ?? 200_000;
164
165
  let state: unknown;
165
166
  const reduceOverflowOneRung = async (msgs: Message[], opts: unknown) => {
166
167
  if (!state) {
@@ -178,7 +179,26 @@ function makeOverflowLadderStub(): {
178
179
  },
179
180
  reduceOverflowOneRung,
180
181
  recoverContextOverflow: async (msgs: Message[], opts: unknown) => {
181
- const step = (await reduceOverflowOneRung(msgs, opts)) as {
182
+ const options = opts as {
183
+ actualTokens: number | null;
184
+ isInteractive: boolean;
185
+ };
186
+ // Mirror ContextWindowManager.deriveOverflowTurnTarget: compute the
187
+ // estimation-error-corrected target and include it in the opts the
188
+ // mock reducer receives, so tests can assert on targetTokens.
189
+ const estimatedInputTokens = estimatePromptTokens(msgs);
190
+ const preflightBudget = Math.floor(maxInputTokens * 0.95);
191
+ const { targetTokens } = computeCorrectedOverflowTarget({
192
+ preflightBudget,
193
+ actualTokens: options.actualTokens,
194
+ estimatedTokens: estimatedInputTokens,
195
+ });
196
+ const enrichedOpts = {
197
+ ...options,
198
+ targetTokens,
199
+ estimatedInputTokens,
200
+ };
201
+ const step = (await reduceOverflowOneRung(msgs, enrichedOpts)) as {
182
202
  messages: Message[];
183
203
  estimatedTokens?: number;
184
204
  state: {
@@ -460,9 +480,11 @@ mock.module("../memory/archive-store.js", () => ({
460
480
  // ── Imports (after mocks) ────────────────────────────────────────────
461
481
 
462
482
  import { AgentLoop } from "../agent/loop.js";
483
+ import { estimatePromptTokens } from "../context/token-estimator.js";
463
484
  import type { Conversation } from "../daemon/conversation.js";
464
485
  import { runAgentLoopImpl } from "../daemon/conversation-agent-loop.js";
465
486
  import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
487
+ import { computeCorrectedOverflowTarget } from "../plugins/defaults/compaction/corrected-target.js";
466
488
  import { asConversation } from "./helpers/mock-conversation.js";
467
489
  import {
468
490
  createMockProvider,
@@ -748,96 +770,83 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
748
770
  });
749
771
 
750
772
  // ── Test 1 ────────────────────────────────────────────────────────
751
- // BUG: When the agent loop makes progress (adds messages to history)
752
- // before hitting context_too_large, the convergence loop's progress
753
- // check must recognize that the loop appended messages. If it fails to,
754
- // the reducer is never invoked — the error is surfaced immediately
755
- // without any compaction attempt.
756
- //
757
- // Expected behavior (PR 2 fix): After progress + context_too_large,
758
- // the system should still attempt compaction before surfacing error.
759
- test.todo(
760
- "context too large after progress triggers compaction retry instead of immediate failure",
761
- async () => {
762
- const events: AssistantEvent[] = [];
763
- let reducerCalled = false;
773
+ // Verifies overflow recovery fires even when the agent made tool-use progress
774
+ // before hitting context_too_large. The convergence path must not skip
775
+ // compaction just because messages were appended earlier in the turn.
776
+ test("context too large after progress triggers compaction retry instead of immediate failure", async () => {
777
+ const events: AssistantEvent[] = [];
778
+ let reducerCalled = false;
764
779
 
765
- mockReducerStepFn = (msgs: Message[]) => {
766
- reducerCalled = true;
767
- return {
780
+ mockReducerStepFn = (msgs: Message[]) => {
781
+ reducerCalled = true;
782
+ return {
783
+ messages: msgs,
784
+ tier: "forced_compaction",
785
+ state: {
786
+ appliedTiers: ["forced_compaction"],
787
+ injectionMode: "full",
788
+ exhausted: false,
789
+ },
790
+ estimatedTokens: 50_000,
791
+ compactionResult: {
792
+ compacted: true,
768
793
  messages: msgs,
769
- tier: "forced_compaction",
770
- state: {
771
- appliedTiers: ["forced_compaction"],
772
- injectionMode: "full",
773
- exhausted: false,
774
- },
775
- estimatedTokens: 50_000,
776
- compactionResult: {
777
- compacted: true,
778
- messages: msgs,
779
- compactedPersistedMessages: 5,
780
- summaryText: "Summary",
781
- previousEstimatedInputTokens: 190_000,
782
- estimatedInputTokens: 50_000,
783
- maxInputTokens: 200_000,
784
- thresholdTokens: 160_000,
785
- compactedMessages: 10,
786
- summaryCalls: 1,
787
- summaryInputTokens: 500,
788
- summaryOutputTokens: 200,
789
- summaryModel: "mock-model",
790
- },
791
- };
794
+ compactedPersistedMessages: 5,
795
+ summaryText: "Summary",
796
+ previousEstimatedInputTokens: 190_000,
797
+ estimatedInputTokens: 50_000,
798
+ maxInputTokens: 200_000,
799
+ thresholdTokens: 160_000,
800
+ compactedMessages: 10,
801
+ summaryCalls: 1,
802
+ summaryInputTokens: 500,
803
+ summaryOutputTokens: 200,
804
+ summaryModel: "mock-model",
805
+ },
792
806
  };
807
+ };
793
808
 
794
- // Run 1 makes progress (a tool turn) then the following provider call
795
- // rejects with a context_too_large error; after the convergence reducer
796
- // compacts, the rerun recovers with plain text.
797
- const { provider } = createMockProvider([
798
- toolUseResponse("tu-progress", "bash", { command: "ls" }),
799
- new Error("prompt is too long: 242201 tokens > 200000 maximum"),
800
- textResponse("recovered after compaction"),
801
- ]);
809
+ // Run 1 makes progress (a tool turn) then the following provider call
810
+ // rejects with a context_too_large error; after the convergence reducer
811
+ // compacts, the rerun recovers with plain text.
812
+ const { provider } = createMockProvider([
813
+ toolUseResponse("tu-progress", "bash", { command: "ls" }),
814
+ new Error("prompt is too long: 242201 tokens > 200000 maximum"),
815
+ textResponse("recovered after compaction"),
816
+ ]);
802
817
 
803
- const ctx = makeCtx({
804
- loopProvider: provider,
805
- loopTools: [
806
- {
807
- name: "bash",
808
- description: "Run a shell command",
809
- input_schema: {
810
- type: "object",
811
- properties: { command: { type: "string" } },
812
- },
818
+ const ctx = makeCtx({
819
+ loopProvider: provider,
820
+ loopTools: [
821
+ {
822
+ name: "bash",
823
+ description: "Run a shell command",
824
+ input_schema: {
825
+ type: "object",
826
+ properties: { command: { type: "string" } },
813
827
  },
814
- ],
815
- toolExecutor: async () => ({
816
- content: "file1.ts\nfile2.ts",
817
- isError: false,
818
- }),
819
- contextWindowManager: {
820
- updateConfig: () => {},
821
- shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
822
- maybeCompact: async () => ({ compacted: false }),
823
- } as unknown as Conversation["contextWindowManager"],
824
- });
828
+ },
829
+ ],
830
+ toolExecutor: async () => ({
831
+ content: "file1.ts\nfile2.ts",
832
+ isError: false,
833
+ }),
834
+ contextWindowManager: {
835
+ updateConfig: () => {},
836
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
837
+ maybeCompact: async () => ({ compacted: false }),
838
+ } as unknown as Conversation["contextWindowManager"],
839
+ });
825
840
 
826
- await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
841
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
827
842
 
828
- // BUG: Currently the reducer is NOT called when progress was made before
829
- // context_too_large. The error is surfaced immediately.
830
- // After PR 2 fix, the reducer SHOULD be called to attempt compaction.
831
- expect(reducerCalled).toBe(true);
843
+ expect(reducerCalled).toBe(true);
832
844
 
833
- // BUG: Currently a conversation_error IS emitted instead of retrying.
834
- // After PR 2 fix, there should be no conversation_error.
835
- const conversationError = events.find(
836
- (e) => e.type === "conversation_error",
837
- );
838
- expect(conversationError).toBeUndefined();
839
- },
840
- );
845
+ const conversationError = events.find(
846
+ (e) => e.type === "conversation_error",
847
+ );
848
+ expect(conversationError).toBeUndefined();
849
+ });
841
850
 
842
851
  // ── Test 2 ────────────────────────────────────────────────────────
843
852
  // When estimation says we're within budget but the provider rejects, the
@@ -922,88 +931,85 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
922
931
  // of 242k (1.31x the estimate of 185k), the target is adjusted downward
923
932
  // based on the observed mismatch (190k / 1.31 ≈ 145k) so the reducer
924
933
  // converges toward the real ceiling rather than the optimistic estimate.
925
- test.todo(
926
- "forced compaction targets a lower budget when estimation has been inaccurate",
927
- async () => {
928
- const events: AssistantEvent[] = [];
929
- let capturedTargetTokens: number | undefined;
934
+ test("forced compaction targets a lower budget when estimation has been inaccurate", async () => {
935
+ const events: AssistantEvent[] = [];
936
+ let capturedTargetTokens: number | undefined;
930
937
 
931
- // Estimator says 185k (below 190k budget = 200k * 0.95)
932
- mockEstimateTokens = 185_000;
938
+ // Estimator says 185k (below 190k budget = 200k * 0.95)
939
+ mockEstimateTokens = 185_000;
933
940
 
934
- // Reducer captures the targetTokens from the config
935
- mockReducerStepFn = (msgs: Message[], cfg: unknown) => {
936
- capturedTargetTokens = (cfg as { targetTokens: number }).targetTokens;
937
- return {
941
+ // Reducer captures the targetTokens from the config
942
+ mockReducerStepFn = (msgs: Message[], cfg: unknown) => {
943
+ capturedTargetTokens = (cfg as { targetTokens: number }).targetTokens;
944
+ return {
945
+ messages: msgs,
946
+ tier: "forced_compaction",
947
+ state: {
948
+ appliedTiers: ["forced_compaction"],
949
+ injectionMode: "full",
950
+ exhausted: false,
951
+ },
952
+ estimatedTokens: 100_000,
953
+ compactionResult: {
954
+ compacted: true,
938
955
  messages: msgs,
939
- tier: "forced_compaction",
940
- state: {
941
- appliedTiers: ["forced_compaction"],
942
- injectionMode: "full",
943
- exhausted: false,
944
- },
945
- estimatedTokens: 100_000,
946
- compactionResult: {
947
- compacted: true,
948
- messages: msgs,
949
- compactedPersistedMessages: 10,
950
- summaryText: "Summary",
951
- previousEstimatedInputTokens: 185_000,
952
- estimatedInputTokens: 100_000,
953
- maxInputTokens: 200_000,
954
- thresholdTokens: 160_000,
955
- compactedMessages: 20,
956
- summaryCalls: 1,
957
- summaryInputTokens: 800,
958
- summaryOutputTokens: 300,
959
- summaryModel: "mock-model",
960
- },
961
- };
956
+ compactedPersistedMessages: 10,
957
+ summaryText: "Summary",
958
+ previousEstimatedInputTokens: 185_000,
959
+ estimatedInputTokens: 100_000,
960
+ maxInputTokens: 200_000,
961
+ thresholdTokens: 160_000,
962
+ compactedMessages: 20,
963
+ summaryCalls: 1,
964
+ summaryInputTokens: 800,
965
+ summaryOutputTokens: 300,
966
+ summaryModel: "mock-model",
967
+ },
962
968
  };
969
+ };
963
970
 
964
- // The provider rejects the first call with a context_too_large error
965
- // (actual tokens 242201, far above the 185k estimate); after forced
966
- // compaction re-targets a lower budget, the rerun recovers with text.
967
- const { provider, calls } = createMockProvider([
968
- new Error("prompt is too long: 242201 tokens > 200000 maximum"),
969
- textResponse("recovered"),
970
- ]);
971
+ // The provider rejects the first call with a context_too_large error
972
+ // (actual tokens 242201, far above the 185k estimate); after forced
973
+ // compaction re-targets a lower budget, the rerun recovers with text.
974
+ const { provider, calls } = createMockProvider([
975
+ new Error("prompt is too long: 242201 tokens > 200000 maximum"),
976
+ textResponse("recovered"),
977
+ ]);
971
978
 
972
- const ctx = makeCtx({
973
- loopProvider: provider,
974
- contextWindowManager: {
975
- updateConfig: () => {},
976
- shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
977
- maybeCompact: async () => ({ compacted: false }),
978
- } as unknown as Conversation["contextWindowManager"],
979
- });
979
+ const ctx = makeCtx({
980
+ loopProvider: provider,
981
+ contextWindowManager: {
982
+ updateConfig: () => {},
983
+ shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
984
+ maybeCompact: async () => ({ compacted: false }),
985
+ } as unknown as Conversation["contextWindowManager"],
986
+ });
980
987
 
981
- await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
988
+ await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
982
989
 
983
- // The reducer should have been called with a corrected target
984
- expect(capturedTargetTokens).toBeDefined();
990
+ // The reducer should have been called with a corrected target
991
+ expect(capturedTargetTokens).toBeDefined();
985
992
 
986
- // preflightBudget = 200_000 * 0.95 = 190_000
987
- // estimationErrorRatio = 242201 / 185000 ≈ 1.309
988
- // correctedTarget = floor(190000 / 1.309) ≈ 145_130
989
- // The corrected target must be LESS than the uncorrected preflightBudget
990
- const preflightBudget = 190_000;
991
- expect(capturedTargetTokens!).toBeLessThan(preflightBudget);
993
+ // preflightBudget = 200_000 * 0.95 = 190_000
994
+ // estimationErrorRatio = 242201 / 185000 ≈ 1.309
995
+ // correctedTarget = floor(190000 / 1.309) ≈ 145_130
996
+ // The corrected target must be LESS than the uncorrected preflightBudget
997
+ const preflightBudget = 190_000;
998
+ expect(capturedTargetTokens!).toBeLessThan(preflightBudget);
992
999
 
993
- // Verify the approximate corrected value (190000 / (242201/185000))
994
- const expectedCorrectedTarget = Math.floor(
995
- preflightBudget / (242201 / 185_000),
996
- );
997
- expect(capturedTargetTokens!).toBe(expectedCorrectedTarget);
1000
+ // Verify the approximate corrected value (190000 / (242201/185000))
1001
+ const expectedCorrectedTarget = Math.floor(
1002
+ preflightBudget / (242201 / 185_000),
1003
+ );
1004
+ expect(capturedTargetTokens!).toBe(expectedCorrectedTarget);
998
1005
 
999
- // Should recover without conversation_error
1000
- const conversationError = events.find(
1001
- (e) => e.type === "conversation_error",
1002
- );
1003
- expect(conversationError).toBeUndefined();
1004
- expect(calls.length).toBe(2);
1005
- },
1006
- );
1006
+ // Should recover without conversation_error
1007
+ const conversationError = events.find(
1008
+ (e) => e.type === "conversation_error",
1009
+ );
1010
+ expect(conversationError).toBeUndefined();
1011
+ expect(calls.length).toBe(2);
1012
+ });
1007
1013
 
1008
1014
  // ── Test 4 ────────────────────────────────────────────────────────
1009
1015
  // A realistic 75+ message conversation with many tool calls where
@@ -12,6 +12,7 @@ mock.module("../subagent/index.js", () => ({
12
12
 
13
13
  import {
14
14
  ConversationEvictor,
15
+ defaultMemoryThresholdBytes,
15
16
  type EvictableConversation,
16
17
  } from "../daemon/conversation-evictor.js";
17
18
 
@@ -268,6 +269,46 @@ describe("ConversationEvictor", () => {
268
269
  expect(resident.disposed).toBe(false);
269
270
  expect(idle.disposed).toBe(true);
270
271
  });
272
+
273
+ test("evicts a bounded batch per sweep, least recently used first", () => {
274
+ for (let i = 0; i < 15; i++) {
275
+ sessions.set(`conv-${i}`, createMockSession());
276
+ }
277
+ evictor = new ConversationEvictor(sessions, {
278
+ ttlMs: Number.MAX_SAFE_INTEGER,
279
+ maxConversations: Number.MAX_SAFE_INTEGER,
280
+ memoryThresholdBytes: 0,
281
+ });
282
+ for (let i = 0; i < 15; i++) {
283
+ evictor.touch(`conv-${i}`);
284
+ }
285
+
286
+ expect(evictor.sweep().memoryEvicted).toBe(10);
287
+ expect(sessions.has("conv-0")).toBe(false);
288
+ expect(sessions.has("conv-10")).toBe(true);
289
+ expect(evictor.sweep().memoryEvicted).toBe(5);
290
+ expect(sessions.size).toBe(0);
291
+ });
292
+
293
+ test("derives the threshold from the container limit at first sweep, not construction", () => {
294
+ const previous = process.env.VELLUM_MEMORY_LIMIT;
295
+ sessions.set("idle", createMockSession());
296
+ evictor = new ConversationEvictor(sessions, {
297
+ ttlMs: Number.MAX_SAFE_INTEGER,
298
+ maxConversations: Number.MAX_SAFE_INTEGER,
299
+ });
300
+ // Half of 1 MiB is far below any live RSS, so the phase must fire.
301
+ process.env.VELLUM_MEMORY_LIMIT = "1Mi";
302
+ try {
303
+ expect(evictor.sweep().memoryEvicted).toBe(1);
304
+ } finally {
305
+ if (previous === undefined) {
306
+ delete process.env.VELLUM_MEMORY_LIMIT;
307
+ } else {
308
+ process.env.VELLUM_MEMORY_LIMIT = previous;
309
+ }
310
+ }
311
+ });
271
312
  });
272
313
 
273
314
  describe("onEvict", () => {
@@ -326,3 +367,16 @@ describe("ConversationEvictor", () => {
326
367
  });
327
368
  });
328
369
  });
370
+
371
+ describe("defaultMemoryThresholdBytes", () => {
372
+ const GIB = 1024 * 1024 * 1024;
373
+
374
+ test("is half the container limit", () => {
375
+ expect(defaultMemoryThresholdBytes(3 * GIB)).toBe(1.5 * GIB);
376
+ expect(defaultMemoryThresholdBytes(8 * GIB)).toBe(4 * GIB);
377
+ });
378
+
379
+ test("falls back to 3 GiB without a limit", () => {
380
+ expect(defaultMemoryThresholdBytes(null)).toBe(3 * GIB);
381
+ });
382
+ });
@@ -116,6 +116,7 @@ mock.module("../config/assistant-initiated-threads-gate.js", () => ({
116
116
  isAssistantInitiatedThreadsEnabled: () => assistantInitiatedThreadsEnabled,
117
117
  }));
118
118
 
119
+ import { resolveAssistantInitiatedThread } from "../notifications/assistant-initiated-thread.js";
119
120
  import { pairDeliveryWithConversation } from "../notifications/conversation-pairing.js";
120
121
  import type { NotificationSignal } from "../notifications/signal.js";
121
122
  import type {
@@ -1478,6 +1479,117 @@ describe("pairDeliveryWithConversation", () => {
1478
1479
  ...overrides,
1479
1480
  });
1480
1481
 
1482
+ test("a prepared resolution is reused even if the source changes before pairing", async () => {
1483
+ assistantInitiatedThreadsEnabled = true;
1484
+ const signal = share({
1485
+ conversationMetadata: { groupId: "group-123" },
1486
+ });
1487
+ const original = structuredClone(signal);
1488
+ const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
1489
+ getConversationMock.mockClear();
1490
+ mockExistingConversations[signal.sourceContextId] = {
1491
+ id: signal.sourceContextId,
1492
+ source: "user",
1493
+ title: null,
1494
+ conversationType: "standard",
1495
+ };
1496
+
1497
+ const result = await pairDeliveryWithConversation(
1498
+ signal,
1499
+ "vellum",
1500
+ makeCopy(),
1501
+ { assistantInitiatedThread },
1502
+ );
1503
+
1504
+ expect(result.createdNewConversation).toBe(true);
1505
+ expect(result.messageId).toBe("msg-001");
1506
+ expect(createConversationMock).toHaveBeenCalledWith(
1507
+ expect.objectContaining({
1508
+ source: "assistant_initiated",
1509
+ groupId: "group-123",
1510
+ }),
1511
+ );
1512
+ expect(getConversationMock).not.toHaveBeenCalled();
1513
+ expect(signal).toEqual(original);
1514
+ });
1515
+
1516
+ test("an ineligible prepared resolution retains passive pairing when the flag changes", async () => {
1517
+ const signal = share();
1518
+ const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
1519
+ assistantInitiatedThreadsEnabled = true;
1520
+ mockExistingConversations[signal.sourceContextId] = {
1521
+ id: signal.sourceContextId,
1522
+ source: "heartbeat",
1523
+ title: null,
1524
+ conversationType: "background",
1525
+ };
1526
+
1527
+ const result = await pairDeliveryWithConversation(
1528
+ signal,
1529
+ "vellum",
1530
+ makeCopy(),
1531
+ { assistantInitiatedThread },
1532
+ );
1533
+
1534
+ expect(result.createdNewConversation).toBe(false);
1535
+ expect(result.conversationId).toBe(signal.sourceContextId);
1536
+ expect(createConversationMock).not.toHaveBeenCalled();
1537
+ expect(addMessageMock).toHaveBeenCalledTimes(1);
1538
+ });
1539
+
1540
+ test.each(["slack", "telegram", "discord"] as const)(
1541
+ "%s retains the producer metadata despite a prepared vellum resolution",
1542
+ async (channel) => {
1543
+ assistantInitiatedThreadsEnabled = true;
1544
+ const signal = share({
1545
+ conversationMetadata: { groupId: "group-123" },
1546
+ });
1547
+ const original = structuredClone(signal);
1548
+ const assistantInitiatedThread =
1549
+ resolveAssistantInitiatedThread(signal);
1550
+ getConversationMock.mockClear();
1551
+
1552
+ const result = await pairDeliveryWithConversation(
1553
+ signal,
1554
+ channel,
1555
+ makeCopy(),
1556
+ { assistantInitiatedThread },
1557
+ );
1558
+
1559
+ expect(result.createdNewConversation).toBe(true);
1560
+ expect(result.strategy).toBe("continue_existing_conversation");
1561
+ expect(createConversationMock).toHaveBeenCalledWith(
1562
+ expect.objectContaining({
1563
+ conversationType: "background",
1564
+ source: "notification",
1565
+ groupId: "group-123",
1566
+ }),
1567
+ );
1568
+ expect(addMessageMock).not.toHaveBeenCalled();
1569
+ expect(getConversationMock).not.toHaveBeenCalled();
1570
+ expect(signal).toEqual(original);
1571
+ },
1572
+ );
1573
+
1574
+ test("platform delivery does not pair a prepared vellum resolution", async () => {
1575
+ assistantInitiatedThreadsEnabled = true;
1576
+ const signal = share();
1577
+ const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
1578
+
1579
+ const result = await pairDeliveryWithConversation(
1580
+ signal,
1581
+ "platform",
1582
+ makeCopy(),
1583
+ { assistantInitiatedThread },
1584
+ );
1585
+
1586
+ expect(result.strategy).toBe("push_only");
1587
+ expect(result.createdNewConversation).toBe(false);
1588
+ expect(result.conversationId).toBeNull();
1589
+ expect(createConversationMock).not.toHaveBeenCalled();
1590
+ expect(addMessageMock).not.toHaveBeenCalled();
1591
+ });
1592
+
1481
1593
  test("flag on: a share from a background run materializes a section thread", async () => {
1482
1594
  assistantInitiatedThreadsEnabled = true;
1483
1595
  mockExistingConversations["conv-heartbeat"] = {
@@ -35,6 +35,7 @@ function bootstrapDocumentTables(): void {
35
35
  const raw = getSqlite();
36
36
  raw.exec(/*sql*/ `
37
37
  DROP TABLE IF EXISTS document_conversations;
38
+ DROP TABLE IF EXISTS document_revisions;
38
39
  DROP TABLE IF EXISTS documents;
39
40
  DROP TABLE IF EXISTS conversations;
40
41
 
@@ -51,9 +52,21 @@ function bootstrapDocumentTables(): void {
51
52
  word_count INTEGER NOT NULL DEFAULT 0,
52
53
  created_at INTEGER NOT NULL,
53
54
  updated_at INTEGER NOT NULL,
55
+ revision INTEGER NOT NULL DEFAULT 0,
54
56
  workspace_path TEXT
55
57
  );
56
58
 
59
+ CREATE TABLE document_revisions (
60
+ surface_id TEXT NOT NULL REFERENCES documents(surface_id) ON DELETE CASCADE,
61
+ revision INTEGER NOT NULL,
62
+ title TEXT NOT NULL,
63
+ content TEXT NOT NULL,
64
+ word_count INTEGER NOT NULL DEFAULT 0,
65
+ author TEXT NOT NULL,
66
+ created_at INTEGER NOT NULL,
67
+ PRIMARY KEY (surface_id, revision)
68
+ );
69
+
57
70
  CREATE TABLE document_conversations (
58
71
  surface_id TEXT NOT NULL,
59
72
  conversation_id TEXT NOT NULL,
@@ -29,6 +29,7 @@ function bootstrapDocumentTables(): void {
29
29
  const raw = getSqlite();
30
30
  raw.exec(/*sql*/ `
31
31
  DROP TABLE IF EXISTS document_conversations;
32
+ DROP TABLE IF EXISTS document_revisions;
32
33
  DROP TABLE IF EXISTS documents;
33
34
  DROP TABLE IF EXISTS conversations;
34
35
 
@@ -45,9 +46,21 @@ function bootstrapDocumentTables(): void {
45
46
  word_count INTEGER NOT NULL DEFAULT 0,
46
47
  created_at INTEGER NOT NULL,
47
48
  updated_at INTEGER NOT NULL,
49
+ revision INTEGER NOT NULL DEFAULT 0,
48
50
  workspace_path TEXT
49
51
  );
50
52
 
53
+ CREATE TABLE document_revisions (
54
+ surface_id TEXT NOT NULL REFERENCES documents(surface_id) ON DELETE CASCADE,
55
+ revision INTEGER NOT NULL,
56
+ title TEXT NOT NULL,
57
+ content TEXT NOT NULL,
58
+ word_count INTEGER NOT NULL DEFAULT 0,
59
+ author TEXT NOT NULL,
60
+ created_at INTEGER NOT NULL,
61
+ PRIMARY KEY (surface_id, revision)
62
+ );
63
+
51
64
  CREATE TABLE document_conversations (
52
65
  surface_id TEXT NOT NULL,
53
66
  conversation_id TEXT NOT NULL,
@@ -28,6 +28,7 @@ function bootstrapDocumentTables(): void {
28
28
  const raw = getSqlite();
29
29
  raw.exec(/*sql*/ `
30
30
  DROP TABLE IF EXISTS document_conversations;
31
+ DROP TABLE IF EXISTS document_revisions;
31
32
  DROP TABLE IF EXISTS documents;
32
33
  DROP TABLE IF EXISTS conversations;
33
34
 
@@ -44,9 +45,21 @@ function bootstrapDocumentTables(): void {
44
45
  word_count INTEGER NOT NULL DEFAULT 0,
45
46
  created_at INTEGER NOT NULL,
46
47
  updated_at INTEGER NOT NULL,
48
+ revision INTEGER NOT NULL DEFAULT 0,
47
49
  workspace_path TEXT
48
50
  );
49
51
 
52
+ CREATE TABLE document_revisions (
53
+ surface_id TEXT NOT NULL REFERENCES documents(surface_id) ON DELETE CASCADE,
54
+ revision INTEGER NOT NULL,
55
+ title TEXT NOT NULL,
56
+ content TEXT NOT NULL,
57
+ word_count INTEGER NOT NULL DEFAULT 0,
58
+ author TEXT NOT NULL,
59
+ created_at INTEGER NOT NULL,
60
+ PRIMARY KEY (surface_id, revision)
61
+ );
62
+
50
63
  CREATE TABLE document_conversations (
51
64
  surface_id TEXT NOT NULL,
52
65
  conversation_id TEXT NOT NULL,