@neurocode-ai/core 1.18.8

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 (490) hide show
  1. package/bin/neurocode +199 -0
  2. package/bunfig.toml +2 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +131 -0
  5. package/schema.json +2071 -0
  6. package/script/fix-node-pty.ts +28 -0
  7. package/script/migration.ts +196 -0
  8. package/src/account/sql.ts +39 -0
  9. package/src/account.ts +101 -0
  10. package/src/agent.ts +111 -0
  11. package/src/aisdk.ts +235 -0
  12. package/src/background-job.ts +365 -0
  13. package/src/catalog.ts +301 -0
  14. package/src/command.ts +64 -0
  15. package/src/config/agent.ts +25 -0
  16. package/src/config/attachments.ts +15 -0
  17. package/src/config/command.ts +12 -0
  18. package/src/config/compaction.ts +15 -0
  19. package/src/config/experimental.ts +18 -0
  20. package/src/config/formatter.ts +12 -0
  21. package/src/config/lsp.ts +18 -0
  22. package/src/config/markdown.ts +36 -0
  23. package/src/config/mcp.ts +48 -0
  24. package/src/config/plugin/agent.ts +179 -0
  25. package/src/config/plugin/command.ts +83 -0
  26. package/src/config/plugin/external.ts +91 -0
  27. package/src/config/plugin/provider.ts +113 -0
  28. package/src/config/plugin/reference.ts +70 -0
  29. package/src/config/plugin/skill.ts +50 -0
  30. package/src/config/plugin.ts +13 -0
  31. package/src/config/provider.ts +71 -0
  32. package/src/config/reference.ts +22 -0
  33. package/src/config/tool-output.ts +9 -0
  34. package/src/config/watcher.ts +7 -0
  35. package/src/config.ts +227 -0
  36. package/src/control-plane/move-session.ts +148 -0
  37. package/src/control-plane/workspace.sql.ts +20 -0
  38. package/src/credential/sql.ts +14 -0
  39. package/src/credential.ts +138 -0
  40. package/src/cross-spawn-spawner.ts +507 -0
  41. package/src/data-migration.sql.ts +6 -0
  42. package/src/database/database.ts +57 -0
  43. package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
  44. package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
  45. package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
  46. package/src/database/migration/20260225215848_workspace.ts +19 -0
  47. package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
  48. package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
  49. package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
  50. package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
  51. package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
  52. package/src/database/migration/20260323234822_events.ts +26 -0
  53. package/src/database/migration/20260410174513_workspace-name.ts +29 -0
  54. package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
  55. package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
  56. package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
  57. package/src/database/migration/20260428004200_add_session_path.ts +11 -0
  58. package/src/database/migration/20260501142318_next_venus.ts +12 -0
  59. package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
  60. package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
  61. package/src/database/migration/20260510033149_session_usage.ts +56 -0
  62. package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
  63. package/src/database/migration/20260511173437_session-metadata.ts +16 -0
  64. package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
  65. package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
  66. package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
  67. package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
  68. package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
  69. package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
  70. package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
  71. package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
  72. package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
  73. package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
  74. package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
  75. package/src/database/migration/20260611035744_credential.ts +25 -0
  76. package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
  77. package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
  78. package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
  79. package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
  80. package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
  81. package/src/database/migration.gen.ts +44 -0
  82. package/src/database/migration.ts +81 -0
  83. package/src/database/path.ts +91 -0
  84. package/src/database/schema.gen.ts +274 -0
  85. package/src/database/schema.sql.ts +10 -0
  86. package/src/database/sqlite.bun.ts +183 -0
  87. package/src/database/sqlite.node.ts +178 -0
  88. package/src/database/sqlite.ts +8 -0
  89. package/src/effect/app-node-builder.ts +23 -0
  90. package/src/effect/app-node-platform.ts +18 -0
  91. package/src/effect/app-node.ts +14 -0
  92. package/src/effect/dfdf +1 -0
  93. package/src/effect/keyed-mutex.ts +45 -0
  94. package/src/effect/layer-node.ts +333 -0
  95. package/src/effect/memo-map.ts +3 -0
  96. package/src/effect/runtime.ts +21 -0
  97. package/src/effect/service-use.ts +43 -0
  98. package/src/event/sql.ts +25 -0
  99. package/src/event.ts +638 -0
  100. package/src/file-mutation.ts +207 -0
  101. package/src/file.ts +6 -0
  102. package/src/filesystem/fff.bun.ts +140 -0
  103. package/src/filesystem/fff.node.ts +138 -0
  104. package/src/filesystem/ignore.ts +67 -0
  105. package/src/filesystem/protected.ts +53 -0
  106. package/src/filesystem/search.ts +239 -0
  107. package/src/filesystem/watcher.ts +140 -0
  108. package/src/filesystem.ts +118 -0
  109. package/src/flag/flag.ts +78 -0
  110. package/src/fs-util.ts +274 -0
  111. package/src/git.ts +987 -0
  112. package/src/github-copilot/README.md +5 -0
  113. package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  114. package/src/github-copilot/chat/get-response-metadata.ts +15 -0
  115. package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  116. package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
  117. package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  118. package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
  119. package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  120. package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  121. package/src/github-copilot/copilot-provider.ts +100 -0
  122. package/src/github-copilot/openai-compatible-error.ts +27 -0
  123. package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
  124. package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  125. package/src/github-copilot/responses/openai-config.ts +18 -0
  126. package/src/github-copilot/responses/openai-error.ts +22 -0
  127. package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
  128. package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
  129. package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
  130. package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
  131. package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
  132. package/src/github-copilot/responses/tool/file-search.ts +127 -0
  133. package/src/github-copilot/responses/tool/image-generation.ts +114 -0
  134. package/src/github-copilot/responses/tool/local-shell.ts +64 -0
  135. package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
  136. package/src/github-copilot/responses/tool/web-search.ts +102 -0
  137. package/src/global.ts +87 -0
  138. package/src/id/id.ts +47 -0
  139. package/src/image/photon.ts +94 -0
  140. package/src/image.ts +81 -0
  141. package/src/installation/version.ts +8 -0
  142. package/src/instruction-context.ts +101 -0
  143. package/src/integration/connection.ts +12 -0
  144. package/src/integration.ts +520 -0
  145. package/src/location-mutation.ts +162 -0
  146. package/src/location-service-map.ts +18 -0
  147. package/src/location-services.ts +115 -0
  148. package/src/location.ts +39 -0
  149. package/src/markdown.d.ts +4 -0
  150. package/src/model.ts +41 -0
  151. package/src/models-dev.ts +260 -0
  152. package/src/npm-config.ts +40 -0
  153. package/src/npm.ts +269 -0
  154. package/src/oauth/page.ts +276 -0
  155. package/src/observability/logging.ts +71 -0
  156. package/src/observability/otlp.ts +79 -0
  157. package/src/observability/shared.ts +1 -0
  158. package/src/observability.ts +24 -0
  159. package/src/patch.ts +197 -0
  160. package/src/permission/saved.ts +79 -0
  161. package/src/permission/sql.ts +20 -0
  162. package/src/permission.ts +310 -0
  163. package/src/plugin/agent.ts +206 -0
  164. package/src/plugin/command/initialize.txt +65 -0
  165. package/src/plugin/command/review.txt +100 -0
  166. package/src/plugin/command.ts +25 -0
  167. package/src/plugin/host.ts +219 -0
  168. package/src/plugin/internal.ts +153 -0
  169. package/src/plugin/layer-map.example.ts +94 -0
  170. package/src/plugin/models-dev.ts +183 -0
  171. package/src/plugin/promise.ts +93 -0
  172. package/src/plugin/provider/alibaba.ts +15 -0
  173. package/src/plugin/provider/amazon-bedrock.ts +126 -0
  174. package/src/plugin/provider/anthropic.ts +27 -0
  175. package/src/plugin/provider/azure.ts +82 -0
  176. package/src/plugin/provider/cerebras.ts +26 -0
  177. package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
  178. package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
  179. package/src/plugin/provider/cohere.ts +15 -0
  180. package/src/plugin/provider/deepinfra.ts +15 -0
  181. package/src/plugin/provider/dynamic.ts +31 -0
  182. package/src/plugin/provider/gateway.ts +15 -0
  183. package/src/plugin/provider/github-copilot.ts +52 -0
  184. package/src/plugin/provider/gitlab.ts +65 -0
  185. package/src/plugin/provider/google-vertex.ts +173 -0
  186. package/src/plugin/provider/google.ts +15 -0
  187. package/src/plugin/provider/groq.ts +15 -0
  188. package/src/plugin/provider/kilo.ts +21 -0
  189. package/src/plugin/provider/llmgateway.ts +26 -0
  190. package/src/plugin/provider/mistral.ts +15 -0
  191. package/src/plugin/provider/nvidia.ts +22 -0
  192. package/src/plugin/provider/openai-compatible.ts +17 -0
  193. package/src/plugin/provider/openai.ts +292 -0
  194. package/src/plugin/provider/opencode.ts +311 -0
  195. package/src/plugin/provider/openrouter.ts +36 -0
  196. package/src/plugin/provider/perplexity.ts +15 -0
  197. package/src/plugin/provider/sap-ai-core.ts +46 -0
  198. package/src/plugin/provider/snowflake-cortex.ts +89 -0
  199. package/src/plugin/provider/togetherai.ts +15 -0
  200. package/src/plugin/provider/venice.ts +15 -0
  201. package/src/plugin/provider/vercel.ts +27 -0
  202. package/src/plugin/provider/xai.ts +22 -0
  203. package/src/plugin/provider/zenmux.ts +21 -0
  204. package/src/plugin/provider.ts +71 -0
  205. package/src/plugin/skill/customize-neurocode.md +452 -0
  206. package/src/plugin/skill.ts +31 -0
  207. package/src/plugin/variant.ts +39 -0
  208. package/src/plugin.ts +167 -0
  209. package/src/policy.ts +49 -0
  210. package/src/process.ts +261 -0
  211. package/src/project/copy-strategies.ts +35 -0
  212. package/src/project/copy.ts +292 -0
  213. package/src/project/directories.ts +158 -0
  214. package/src/project/schema.ts +16 -0
  215. package/src/project/sql.ts +35 -0
  216. package/src/project.ts +136 -0
  217. package/src/provider.ts +25 -0
  218. package/src/pty/protocol.ts +37 -0
  219. package/src/pty/pty.bun.ts +26 -0
  220. package/src/pty/pty.node.ts +29 -0
  221. package/src/pty/pty.ts +25 -0
  222. package/src/pty/schema.ts +1 -0
  223. package/src/pty/ticket.ts +56 -0
  224. package/src/pty.ts +318 -0
  225. package/src/public-event-manifest.ts +7 -0
  226. package/src/question.ts +153 -0
  227. package/src/reference/guidance.ts +69 -0
  228. package/src/reference.ts +125 -0
  229. package/src/repository-cache.ts +259 -0
  230. package/src/repository.ts +214 -0
  231. package/src/ripgrep/binary.ts +132 -0
  232. package/src/ripgrep.ts +281 -0
  233. package/src/schema.ts +79 -0
  234. package/src/session/compaction.ts +241 -0
  235. package/src/session/context-epoch.ts +174 -0
  236. package/src/session/error.ts +24 -0
  237. package/src/session/event.ts +2 -0
  238. package/src/session/execution/local.ts +46 -0
  239. package/src/session/execution.ts +34 -0
  240. package/src/session/history.ts +101 -0
  241. package/src/session/info.ts +50 -0
  242. package/src/session/input.ts +288 -0
  243. package/src/session/message-updater.ts +397 -0
  244. package/src/session/message.ts +2 -0
  245. package/src/session/projector.ts +458 -0
  246. package/src/session/prompt.ts +1 -0
  247. package/src/session/revert.ts +121 -0
  248. package/src/session/run-coordinator.ts +104 -0
  249. package/src/session/runner/index.ts +28 -0
  250. package/src/session/runner/llm.ts +432 -0
  251. package/src/session/runner/max-steps.ts +16 -0
  252. package/src/session/runner/model.ts +218 -0
  253. package/src/session/runner/publish-llm-event.ts +423 -0
  254. package/src/session/runner/to-llm-message.ts +171 -0
  255. package/src/session/schema.ts +9 -0
  256. package/src/session/sql.ts +176 -0
  257. package/src/session/store.ts +63 -0
  258. package/src/session/todo.ts +78 -0
  259. package/src/session.ts +486 -0
  260. package/src/share/sql.ts +13 -0
  261. package/src/shell.ts +226 -0
  262. package/src/skill/discovery.ts +213 -0
  263. package/src/skill/guidance.ts +76 -0
  264. package/src/skill.ts +132 -0
  265. package/src/snapshot.ts +266 -0
  266. package/src/state.ts +128 -0
  267. package/src/system-context/builtins.ts +50 -0
  268. package/src/system-context/index.ts +320 -0
  269. package/src/system-context/registry.ts +49 -0
  270. package/src/tool/AGENTS.md +59 -0
  271. package/src/tool/application-tools.ts +57 -0
  272. package/src/tool/apply-patch.ts +219 -0
  273. package/src/tool/bash.ts +207 -0
  274. package/src/tool/builtins.ts +48 -0
  275. package/src/tool/edit.ts +223 -0
  276. package/src/tool/glob.ts +105 -0
  277. package/src/tool/grep.ts +137 -0
  278. package/src/tool/http-body.ts +30 -0
  279. package/src/tool/question.ts +94 -0
  280. package/src/tool/read-filesystem.ts +366 -0
  281. package/src/tool/read.ts +117 -0
  282. package/src/tool/registry.ts +147 -0
  283. package/src/tool/skill.ts +109 -0
  284. package/src/tool/todowrite.ts +62 -0
  285. package/src/tool/tool.ts +162 -0
  286. package/src/tool/tools.ts +13 -0
  287. package/src/tool/webfetch.ts +218 -0
  288. package/src/tool/websearch.ts +260 -0
  289. package/src/tool/write.ts +101 -0
  290. package/src/tool-output-store.ts +211 -0
  291. package/src/util/array.ts +10 -0
  292. package/src/util/binary.ts +41 -0
  293. package/src/util/effect-flock.ts +284 -0
  294. package/src/util/encode.ts +51 -0
  295. package/src/util/error.ts +70 -0
  296. package/src/util/flock.ts +358 -0
  297. package/src/util/glob.ts +34 -0
  298. package/src/util/hash.ts +11 -0
  299. package/src/util/identifier.ts +1 -0
  300. package/src/util/iife.ts +3 -0
  301. package/src/util/lazy.ts +11 -0
  302. package/src/util/module.ts +10 -0
  303. package/src/util/path.ts +37 -0
  304. package/src/util/retry.ts +42 -0
  305. package/src/util/slug.ts +74 -0
  306. package/src/util/token.ts +5 -0
  307. package/src/util/which.ts +14 -0
  308. package/src/util/wildcard.ts +14 -0
  309. package/src/v1/config/agent.ts +89 -0
  310. package/src/v1/config/attachment.ts +25 -0
  311. package/src/v1/config/command.ts +13 -0
  312. package/src/v1/config/config.ts +192 -0
  313. package/src/v1/config/console-state.ts +16 -0
  314. package/src/v1/config/error.ts +39 -0
  315. package/src/v1/config/formatter.ts +13 -0
  316. package/src/v1/config/layout.ts +6 -0
  317. package/src/v1/config/lsp.ts +80 -0
  318. package/src/v1/config/mcp.ts +63 -0
  319. package/src/v1/config/migrate.ts +254 -0
  320. package/src/v1/config/permission.ts +50 -0
  321. package/src/v1/config/plugin.ts +9 -0
  322. package/src/v1/config/provider-options.ts +227 -0
  323. package/src/v1/config/provider.ts +121 -0
  324. package/src/v1/config/server.ts +19 -0
  325. package/src/v1/config/skills.ts +13 -0
  326. package/src/v1/permission.ts +33 -0
  327. package/src/v1/session.ts +68 -0
  328. package/src/v2-schema.ts +3 -0
  329. package/src/workspace.ts +6 -0
  330. package/sst-env.d.ts +10 -0
  331. package/test/agent.test.ts +131 -0
  332. package/test/application-tools.test.ts +287 -0
  333. package/test/background-job.test.ts +106 -0
  334. package/test/catalog.test.ts +353 -0
  335. package/test/command.test.ts +56 -0
  336. package/test/config/agent.test.ts +349 -0
  337. package/test/config/command.test.ts +83 -0
  338. package/test/config/config.test.ts +798 -0
  339. package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
  340. package/test/config/plugin.test.ts +248 -0
  341. package/test/config/provider-options.test.ts +224 -0
  342. package/test/config/provider.test.ts +269 -0
  343. package/test/config/skill.test.ts +80 -0
  344. package/test/credential.test.ts +36 -0
  345. package/test/database-migration.test.ts +644 -0
  346. package/test/effect/cross-spawn-spawner.test.ts +426 -0
  347. package/test/effect/keyed-mutex.test.ts +73 -0
  348. package/test/effect/layer-node/layer-node-types.test.ts +143 -0
  349. package/test/effect/layer-node/layer-node.test.ts +261 -0
  350. package/test/effect/layer-node/node-build.test.ts +125 -0
  351. package/test/effect/observability.test.ts +109 -0
  352. package/test/event.test.ts +1124 -0
  353. package/test/file-mutation.test.ts +368 -0
  354. package/test/filesystem/filesystem.test.ts +387 -0
  355. package/test/filesystem/ignore.test.ts +10 -0
  356. package/test/filesystem/search.test.ts +44 -0
  357. package/test/filesystem/watcher.test.ts +266 -0
  358. package/test/fixture/effect-flock-worker.ts +60 -0
  359. package/test/fixture/flock-worker.ts +72 -0
  360. package/test/fixture/git.ts +49 -0
  361. package/test/fixture/location.ts +26 -0
  362. package/test/fixture/tmpdir.ts +25 -0
  363. package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
  364. package/test/git.test.ts +164 -0
  365. package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
  366. package/test/github-copilot/copilot-chat-model.test.ts +592 -0
  367. package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
  368. package/test/global.test.ts +16 -0
  369. package/test/instruction-context.test.ts +323 -0
  370. package/test/integration.test.ts +349 -0
  371. package/test/legacy-event-schema.test.ts +16 -0
  372. package/test/lib/effect.ts +53 -0
  373. package/test/lib/tool.ts +20 -0
  374. package/test/location-filesystem.test.ts +69 -0
  375. package/test/location-layer.test.ts +226 -0
  376. package/test/location-mutation.test.ts +178 -0
  377. package/test/location.test.ts +42 -0
  378. package/test/model.test.ts +23 -0
  379. package/test/models.test.ts +290 -0
  380. package/test/move-session.test.ts +235 -0
  381. package/test/npm-config.test.ts +51 -0
  382. package/test/npm.test.ts +84 -0
  383. package/test/oauth-page.test.ts +15 -0
  384. package/test/patch.test.ts +68 -0
  385. package/test/permission.test.ts +315 -0
  386. package/test/plugin/command.test.ts +47 -0
  387. package/test/plugin/fixture.ts +52 -0
  388. package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
  389. package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
  390. package/test/plugin/fixtures/invalid-plugin.ts +1 -0
  391. package/test/plugin/fixtures/models-dev.json +14 -0
  392. package/test/plugin/fixtures/provider-factory.ts +9 -0
  393. package/test/plugin/host.ts +303 -0
  394. package/test/plugin/models-dev.test.ts +170 -0
  395. package/test/plugin/promise.test.ts +67 -0
  396. package/test/plugin/provider-alibaba.test.ts +92 -0
  397. package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
  398. package/test/plugin/provider-anthropic.test.ts +94 -0
  399. package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
  400. package/test/plugin/provider-azure.test.ts +293 -0
  401. package/test/plugin/provider-cerebras.test.ts +136 -0
  402. package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
  403. package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
  404. package/test/plugin/provider-cohere.test.ts +123 -0
  405. package/test/plugin/provider-deepinfra.test.ts +159 -0
  406. package/test/plugin/provider-dynamic.test.ts +184 -0
  407. package/test/plugin/provider-gateway.test.ts +115 -0
  408. package/test/plugin/provider-github-copilot.test.ts +278 -0
  409. package/test/plugin/provider-gitlab.test.ts +308 -0
  410. package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
  411. package/test/plugin/provider-google-vertex.test.ts +387 -0
  412. package/test/plugin/provider-google.test.ts +80 -0
  413. package/test/plugin/provider-groq.test.ts +121 -0
  414. package/test/plugin/provider-kilo.test.ts +99 -0
  415. package/test/plugin/provider-llmgateway.test.ts +80 -0
  416. package/test/plugin/provider-mistral.test.ts +129 -0
  417. package/test/plugin/provider-nvidia.test.ts +97 -0
  418. package/test/plugin/provider-openai-compatible.test.ts +107 -0
  419. package/test/plugin/provider-openai.test.ts +176 -0
  420. package/test/plugin/provider-opencode.test.ts +423 -0
  421. package/test/plugin/provider-openrouter.test.ts +113 -0
  422. package/test/plugin/provider-perplexity.test.ts +122 -0
  423. package/test/plugin/provider-sap-ai-core.test.ts +159 -0
  424. package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
  425. package/test/plugin/provider-togetherai.test.ts +129 -0
  426. package/test/plugin/provider-venice.test.ts +115 -0
  427. package/test/plugin/provider-vercel.test.ts +83 -0
  428. package/test/plugin/provider-xai.test.ts +125 -0
  429. package/test/plugin/provider-zenmux.test.ts +110 -0
  430. package/test/plugin/skill.test.ts +25 -0
  431. package/test/plugin/variant.test.ts +67 -0
  432. package/test/plugin.test.ts +71 -0
  433. package/test/policy.test.ts +85 -0
  434. package/test/preload.ts +5 -0
  435. package/test/process/process.test.ts +368 -0
  436. package/test/project-copy.test.ts +400 -0
  437. package/test/project-directories.test.ts +61 -0
  438. package/test/project.test.ts +221 -0
  439. package/test/provider-mistral.test.ts +282 -0
  440. package/test/provider-xai-responses.test.ts +32 -0
  441. package/test/pty/info-schema.test.ts +32 -0
  442. package/test/pty/protocol.test.ts +27 -0
  443. package/test/pty/pty-session.test.ts +240 -0
  444. package/test/pty/ticket.test.ts +62 -0
  445. package/test/question.test.ts +114 -0
  446. package/test/reference-guidance.test.ts +76 -0
  447. package/test/reference.test.ts +78 -0
  448. package/test/repository-cache.test.ts +153 -0
  449. package/test/repository.test.ts +71 -0
  450. package/test/ripgrep.test.ts +65 -0
  451. package/test/session-compaction.test.ts +27 -0
  452. package/test/session-create.test.ts +425 -0
  453. package/test/session-history.test.ts +165 -0
  454. package/test/session-projector.test.ts +533 -0
  455. package/test/session-prompt.test.ts +584 -0
  456. package/test/session-run-coordinator.test.ts +418 -0
  457. package/test/session-runner-message.test.ts +501 -0
  458. package/test/session-runner-model.test.ts +347 -0
  459. package/test/session-runner-recorded.test.ts +193 -0
  460. package/test/session-runner-tool-events.test.ts +136 -0
  461. package/test/session-runner-tool-registry.test.ts +452 -0
  462. package/test/session-runner.test.ts +3365 -0
  463. package/test/session-todo.test.ts +94 -0
  464. package/test/session-tool-progress.test.ts +158 -0
  465. package/test/shared-schema.test.ts +206 -0
  466. package/test/shell.test.ts +108 -0
  467. package/test/skill/guidance.test.ts +144 -0
  468. package/test/skill-discovery.test.ts +165 -0
  469. package/test/skill.test.ts +125 -0
  470. package/test/snapshot.test.ts +178 -0
  471. package/test/state.test.ts +115 -0
  472. package/test/system-context/builtins.test.ts +129 -0
  473. package/test/system-context/index.test.ts +307 -0
  474. package/test/system-context/registry.test.ts +114 -0
  475. package/test/tool-apply-patch.test.ts +436 -0
  476. package/test/tool-bash.test.ts +437 -0
  477. package/test/tool-edit.test.ts +434 -0
  478. package/test/tool-output-store.test.ts +247 -0
  479. package/test/tool-question.test.ts +162 -0
  480. package/test/tool-read-filesystem.test.ts +118 -0
  481. package/test/tool-read.test.ts +674 -0
  482. package/test/tool-skill.test.ts +149 -0
  483. package/test/tool-todowrite.test.ts +125 -0
  484. package/test/tool-webfetch.test.ts +281 -0
  485. package/test/tool-websearch.test.ts +316 -0
  486. package/test/tool-write.test.ts +305 -0
  487. package/test/util/effect-flock.test.ts +392 -0
  488. package/test/util/flock.test.ts +428 -0
  489. package/test/util/which.test.ts +100 -0
  490. package/tsconfig.json +7 -0
@@ -0,0 +1,644 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { $ } from "bun"
3
+ import { fileURLToPath } from "url"
4
+ import path from "path"
5
+ import { SqliteClient } from "@effect/sql-sqlite-bun"
6
+ import { EffectDrizzleSqlite } from "@neurocode-ai/effect-drizzle-sqlite"
7
+ import { Effect, Layer } from "effect"
8
+ import { eq, inArray, sql } from "drizzle-orm"
9
+ import { DatabaseMigration } from "@neurocode-ai/core/database/migration"
10
+ import { migrations } from "@neurocode-ai/core/database/migration.gen"
11
+ import sessionUsageMigration from "@neurocode-ai/core/database/migration/20260510033149_session_usage"
12
+ import normalizeStoragePathsMigration from "@neurocode-ai/core/database/migration/20260601010001_normalize_storage_paths"
13
+ import sessionMessageProjectionOrderMigration from "@neurocode-ai/core/database/migration/20260603040000_session_message_projection_order"
14
+ import eventSourcedSessionInputMigration from "@neurocode-ai/core/database/migration/20260604172448_event_sourced_session_input"
15
+ import contextEpochAgentMigration from "@neurocode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
16
+ import simplifyIntegrationCredentialsMigration from "@neurocode-ai/core/database/migration/20260611192811_lush_chimera"
17
+ import simplifySessionInputMigration from "@neurocode-ai/core/database/migration/20260622202450_simplify_session_input"
18
+ import { AppNodeBuilder } from "@neurocode-ai/core/effect/app-node-builder"
19
+ import { LayerNode } from "@neurocode-ai/core/effect/layer-node"
20
+ import { EventV2 } from "@neurocode-ai/core/event"
21
+ import { ProjectV2 } from "@neurocode-ai/core/project"
22
+ import { ProjectTable } from "@neurocode-ai/core/project/sql"
23
+ import { AbsolutePath } from "@neurocode-ai/core/schema"
24
+ import { SessionSchema } from "@neurocode-ai/core/session/schema"
25
+ import { SessionTable } from "@neurocode-ai/core/session/sql"
26
+ import sessionMetadataMigration from "@neurocode-ai/core/database/migration/20260511173437_session-metadata"
27
+ import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
28
+ import { Database } from "@neurocode-ai/core/database/database"
29
+ import { SessionProjector } from "@neurocode-ai/core/session/projector"
30
+ import { SessionV1 } from "@neurocode-ai/core/v1/session"
31
+ import { tmpdir } from "./fixture/tmpdir"
32
+
33
+ const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
34
+ Effect.runPromise(
35
+ effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
36
+ )
37
+
38
+ const makeDb = EffectDrizzleSqlite.makeWithDefaults()
39
+
40
+ describe("DatabaseMigration", () => {
41
+ test("serializes concurrent embedded initialization for one database path", async () => {
42
+ await using tmp = await tmpdir()
43
+ const filename = path.join(tmp.path, "embedded.sqlite")
44
+ const layers = [Database.layerFromPath(filename), Database.layerFromPath(filename)]
45
+
46
+ await Effect.runPromise(
47
+ Effect.all(
48
+ layers.map((layer) => Effect.scoped(Layer.build(layer))),
49
+ { concurrency: "unbounded" },
50
+ ),
51
+ )
52
+ })
53
+ if (process.platform === "linux") {
54
+ test("declared schema has no ungenerated migrations", async () => {
55
+ const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`
56
+ .quiet()
57
+ .nothrow()
58
+ expect(result.exitCode, result.stderr.toString()).toBe(0)
59
+ expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate")
60
+ }, 30_000)
61
+ }
62
+
63
+ test("applies tracked migrations to an empty database", async () => {
64
+ await run(
65
+ Effect.gen(function* () {
66
+ const db = yield* makeDb
67
+ yield* DatabaseMigration.apply(db)
68
+
69
+ expect(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`)).toEqual({
70
+ name: "session",
71
+ })
72
+ expect(
73
+ yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input'`),
74
+ ).toEqual({ name: "session_input" })
75
+ expect(
76
+ yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_context_epoch'`),
77
+ ).toEqual({ name: "session_context_epoch" })
78
+ expect(
79
+ yield* db.get(
80
+ sql`SELECT name FROM pragma_table_info('session_context_epoch') WHERE name IN ('agent', 'replacement_seq', 'revision')`,
81
+ ),
82
+ ).toBeUndefined()
83
+ expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: migrations.length })
84
+ expect(
85
+ yield* db.all(
86
+ sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('event_aggregate_seq_idx', 'event_aggregate_type_seq_idx', 'session_input_session_pending_seq_idx', 'session_input_session_pending_delivery_seq_idx', 'session_input_session_admitted_seq_idx', 'session_input_session_promoted_seq_idx', 'session_message_session_idx', 'session_message_session_type_idx', 'session_message_session_seq_idx', 'session_message_session_type_seq_idx', 'session_message_session_time_created_id_idx') ORDER BY name`,
87
+ ),
88
+ ).toEqual([
89
+ { name: "event_aggregate_seq_idx" },
90
+ { name: "event_aggregate_type_seq_idx" },
91
+ { name: "session_input_session_admitted_seq_idx" },
92
+ { name: "session_input_session_pending_delivery_seq_idx" },
93
+ { name: "session_input_session_promoted_seq_idx" },
94
+ { name: "session_message_session_seq_idx" },
95
+ { name: "session_message_session_time_created_id_idx" },
96
+ { name: "session_message_session_type_seq_idx" },
97
+ ])
98
+ }),
99
+ )
100
+ })
101
+
102
+ test("rejects a non-empty database without a session table", async () => {
103
+ await expect(
104
+ run(
105
+ Effect.gen(function* () {
106
+ const db = yield* makeDb
107
+ yield* db.run(sql`CREATE TABLE unrelated (id text PRIMARY KEY)`)
108
+ yield* DatabaseMigration.apply(db)
109
+ }),
110
+ ),
111
+ ).rejects.toThrow("Database is not empty and has no session table")
112
+ })
113
+
114
+ test("backfills existing Context Epoch rows to the build agent", async () => {
115
+ await run(
116
+ Effect.gen(function* () {
117
+ const db = yield* makeDb
118
+ yield* db.run(
119
+ sql`CREATE TABLE session_context_epoch (session_id text PRIMARY KEY, baseline text NOT NULL, snapshot text NOT NULL, baseline_seq integer NOT NULL, replacement_seq integer, revision integer DEFAULT 0 NOT NULL)`,
120
+ )
121
+ yield* db.run(
122
+ sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('ses_existing', 'baseline', '{}', 0)`,
123
+ )
124
+
125
+ yield* DatabaseMigration.applyOnly(db, [contextEpochAgentMigration])
126
+
127
+ expect(yield* db.get(sql`SELECT agent FROM session_context_epoch WHERE session_id = 'ses_existing'`)).toEqual({
128
+ agent: "build",
129
+ })
130
+ }),
131
+ )
132
+ })
133
+
134
+ test("keeps legacy credential fields nullable", async () => {
135
+ await run(
136
+ Effect.gen(function* () {
137
+ const db = yield* makeDb
138
+ yield* db.run(
139
+ sql`CREATE TABLE credential (id text PRIMARY KEY, connector_id text NOT NULL, method_id text NOT NULL, label text NOT NULL, value text NOT NULL, active integer DEFAULT false NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL)`,
140
+ )
141
+ yield* db.run(
142
+ sql`CREATE UNIQUE INDEX credential_connector_active_idx ON credential (connector_id) WHERE active = 1`,
143
+ )
144
+ yield* DatabaseMigration.applyOnly(db, [simplifyIntegrationCredentialsMigration])
145
+
146
+ yield* db.run(
147
+ sql`INSERT INTO credential (id, connector_id, method_id, label, value, active, time_created, time_updated) VALUES ('legacy', 'openai', 'oauth', 'Legacy', '{}', 1, 1, 1)`,
148
+ )
149
+ yield* db.run(
150
+ sql`INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES ('current', 'anthropic', 'Current', '{}', 2, 2)`,
151
+ )
152
+ expect(yield* db.get(sql`SELECT connector_id, method_id, active FROM credential WHERE id = 'current'`)).toEqual(
153
+ { connector_id: null, method_id: null, active: null },
154
+ )
155
+ }),
156
+ )
157
+ })
158
+
159
+ test("resets beta history and rebuilds event-sourced Session input storage", async () => {
160
+ await run(
161
+ Effect.gen(function* () {
162
+ const db = yield* makeDb
163
+ yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, workspace_id text)`)
164
+ yield* db.run(sql`CREATE TABLE workspace (id text PRIMARY KEY)`)
165
+ yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY)`)
166
+ yield* db.run(sql`CREATE TABLE part (id text PRIMARY KEY)`)
167
+ yield* db.run(sql`CREATE TABLE event_sequence (aggregate_id text PRIMARY KEY, seq integer NOT NULL)`)
168
+ yield* db.run(
169
+ sql`CREATE TABLE event (id text PRIMARY KEY, aggregate_id text NOT NULL, seq integer NOT NULL, type text NOT NULL, data text NOT NULL)`,
170
+ )
171
+ yield* db.run(sql`CREATE INDEX event_aggregate_seq_idx ON event (aggregate_id, seq)`)
172
+ yield* db.run(sql`CREATE INDEX event_aggregate_type_seq_idx ON event (aggregate_id, type, seq)`)
173
+ yield* db.run(
174
+ sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
175
+ )
176
+ yield* db.run(sql`CREATE INDEX session_message_session_seq_idx ON session_message (session_id, seq)`)
177
+ yield* db.run(
178
+ sql`CREATE TABLE session_input (seq integer PRIMARY KEY AUTOINCREMENT, id text NOT NULL UNIQUE, session_id text NOT NULL, prompt text NOT NULL, delivery text NOT NULL, promoted_seq integer, time_created integer NOT NULL)`,
179
+ )
180
+ yield* db.run(
181
+ sql`CREATE INDEX session_input_session_pending_delivery_seq_idx ON session_input (session_id, promoted_seq, delivery, seq)`,
182
+ )
183
+ yield* db.run(sql`INSERT INTO session (id, workspace_id) VALUES ('session', 'wrk_old')`)
184
+ yield* db.run(sql`INSERT INTO workspace (id) VALUES ('wrk_old')`)
185
+ yield* db.run(sql`INSERT INTO message (id) VALUES ('message')`)
186
+ yield* db.run(sql`INSERT INTO part (id) VALUES ('part')`)
187
+ yield* db.run(sql`INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 0)`)
188
+ yield* db.run(
189
+ sql`INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('evt_old', 'session', 0, 'old.1', '{}')`,
190
+ )
191
+ yield* db.run(
192
+ sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('msg_old', 'session', 'user', 0, 1, 1, '{}')`,
193
+ )
194
+ yield* db.run(
195
+ sql`INSERT INTO session_input (id, session_id, prompt, delivery, time_created) VALUES ('msg_pending', 'session', '{}', 'steer', 1)`,
196
+ )
197
+
198
+ yield* DatabaseMigration.applyOnly(db, [eventSourcedSessionInputMigration])
199
+
200
+ expect(yield* db.all(sql`SELECT id, workspace_id FROM session`)).toEqual([
201
+ { id: "session", workspace_id: null },
202
+ ])
203
+ expect(yield* db.all(sql`SELECT id FROM workspace`)).toEqual([])
204
+ expect(yield* db.all(sql`SELECT id FROM message`)).toEqual([{ id: "message" }])
205
+ expect(yield* db.all(sql`SELECT id FROM part`)).toEqual([{ id: "part" }])
206
+ expect(yield* db.all(sql`SELECT id FROM event`)).toEqual([])
207
+ expect(yield* db.all(sql`SELECT aggregate_id FROM event_sequence`)).toEqual([])
208
+ expect(yield* db.all(sql`SELECT id FROM session_message`)).toEqual([])
209
+ expect(yield* db.all(sql`SELECT id FROM session_input`)).toEqual([])
210
+ expect(
211
+ (yield* db.all<{ name: string }>(sql`PRAGMA table_info(session_input)`)).map((column) => column.name),
212
+ ).toEqual(["id", "session_id", "prompt", "delivery", "admitted_seq", "promoted_seq", "time_created"])
213
+ expect(
214
+ (yield* db.all<{ name: string; unique: number }>(sql`PRAGMA index_list(session_message)`)).find(
215
+ (index) => index.name === "session_message_session_seq_idx",
216
+ ),
217
+ ).toMatchObject({ unique: 1 })
218
+ expect(
219
+ (yield* db.all<{ name: string; unique: number }>(sql`PRAGMA index_list(event)`)).find(
220
+ (index) => index.name === "event_aggregate_seq_idx",
221
+ ),
222
+ ).toMatchObject({ unique: 1 })
223
+ expect(
224
+ (yield* db.all<{ name: string; unique: number }>(sql`PRAGMA index_list(session_input)`)).filter((index) =>
225
+ ["session_input_session_admitted_seq_idx", "session_input_session_promoted_seq_idx"].includes(index.name),
226
+ ),
227
+ ).toEqual([
228
+ expect.objectContaining({ name: "session_input_session_promoted_seq_idx", unique: 1 }),
229
+ expect.objectContaining({ name: "session_input_session_admitted_seq_idx", unique: 1 }),
230
+ ])
231
+ }),
232
+ )
233
+ })
234
+
235
+ test("preserves canonical V1 state and restarts its event stream", async () => {
236
+ await run(
237
+ Effect.gen(function* () {
238
+ const db = yield* makeDb
239
+ yield* db.run(sql`PRAGMA foreign_keys = ON`)
240
+ yield* DatabaseMigration.apply(db)
241
+ yield* db.run(
242
+ sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('global', '/project', 1, 1, '[]')`,
243
+ )
244
+ yield* db.run(
245
+ sql`INSERT INTO workspace (id, type, project_id, time_used) VALUES ('workspace', 'local', 'global', 1)`,
246
+ )
247
+ yield* db.run(
248
+ sql`INSERT INTO session (id, project_id, workspace_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'global', 'workspace', 'session', '/project', 'Before', 'test', 1, 1)`,
249
+ )
250
+ yield* db.run(
251
+ sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('message', 'session', 1, 1, '{}')`,
252
+ )
253
+ yield* db.run(
254
+ sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('part', 'message', 'session', 1, 1, '{}')`,
255
+ )
256
+ yield* db.run(sql`INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 9)`)
257
+ yield* db.run(
258
+ sql`INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('event', 'session', 9, 'session.updated.1', '{}')`,
259
+ )
260
+ yield* db.run(
261
+ sql`INSERT INTO session_input (id, session_id, prompt, delivery, admitted_seq, time_created) VALUES ('input', 'session', '{}', 'steer', 9, 1)`,
262
+ )
263
+ yield* db.run(
264
+ sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
265
+ )
266
+ yield* db.run(
267
+ sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
268
+ )
269
+ yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionInputMigration.id}`)
270
+ yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
271
+
272
+ const database = Layer.succeed(Database.Service, { db })
273
+ yield* EventV2.Service.use((service) =>
274
+ service.publish(SessionV1.Event.Updated, {
275
+ sessionID: SessionSchema.ID.make("session"),
276
+ info: {
277
+ id: SessionSchema.ID.make("session"),
278
+ slug: "session",
279
+ projectID: ProjectV2.ID.global,
280
+ directory: "/project",
281
+ title: "After",
282
+ version: "test",
283
+ time: { created: 1, updated: 2 },
284
+ },
285
+ }),
286
+ ).pipe(
287
+ Effect.provide(
288
+ AppNodeBuilder.build(LayerNode.group([EventV2.node, SessionProjector.node]), [[Database.node, database]]),
289
+ ),
290
+ )
291
+
292
+ expect(
293
+ yield* db.get(sql`
294
+ SELECT
295
+ (SELECT title FROM session WHERE id = 'session') AS title,
296
+ (SELECT workspace_id FROM session WHERE id = 'session') AS workspaceID,
297
+ (SELECT COUNT(*) FROM message WHERE id = 'message') AS messages,
298
+ (SELECT COUNT(*) FROM part WHERE id = 'part') AS parts,
299
+ (SELECT COUNT(*) FROM workspace) AS workspaces,
300
+ (SELECT COUNT(*) FROM session_input) AS sessionInputs,
301
+ (SELECT COUNT(*) FROM session_message) AS sessionMessages,
302
+ (SELECT COUNT(*) FROM session_context_epoch) AS contextEpochs,
303
+ (SELECT seq FROM event_sequence WHERE aggregate_id = 'session') AS seq,
304
+ (SELECT type FROM event WHERE aggregate_id = 'session') AS eventType
305
+ `),
306
+ ).toEqual({
307
+ title: "After",
308
+ workspaceID: null,
309
+ messages: 1,
310
+ parts: 1,
311
+ workspaces: 0,
312
+ sessionInputs: 0,
313
+ sessionMessages: 0,
314
+ contextEpochs: 0,
315
+ seq: 0,
316
+ eventType: "session.updated.1",
317
+ })
318
+ }),
319
+ )
320
+ })
321
+
322
+ test("resets incompatible projected Session messages before adding sequence order", async () => {
323
+ await run(
324
+ Effect.gen(function* () {
325
+ const db = yield* makeDb
326
+ yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
327
+ yield* db.run(
328
+ sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
329
+ )
330
+ yield* db.run(
331
+ sql`CREATE TABLE part (id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
332
+ )
333
+ yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY, seq integer NOT NULL)`)
334
+ yield* db.run(
335
+ sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
336
+ )
337
+ yield* db.run(
338
+ sql`CREATE INDEX session_message_session_time_created_id_idx ON session_message (session_id, time_created, id)`,
339
+ )
340
+ yield* db.run(
341
+ sql`CREATE INDEX session_message_session_type_time_created_id_idx ON session_message (session_id, type, time_created, id)`,
342
+ )
343
+ yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`)
344
+ yield* db.run(
345
+ sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('legacy_message', 'session', 1, 1, '{"role":"user"}')`,
346
+ )
347
+ yield* db.run(
348
+ sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('legacy_part', 'legacy_message', 'session', 1, 1, '{"type":"text","text":"hello"}')`,
349
+ )
350
+ yield* db.run(
351
+ sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('stale_projection', 'session', 'user', 1, 1, '{}')`,
352
+ )
353
+
354
+ yield* DatabaseMigration.applyOnly(db, [sessionMessageProjectionOrderMigration])
355
+
356
+ expect(yield* db.all(sql`SELECT id, session_id, data FROM message`)).toEqual([
357
+ { id: "legacy_message", session_id: "session", data: '{"role":"user"}' },
358
+ ])
359
+ expect(yield* db.all(sql`SELECT id, message_id, session_id, data FROM part`)).toEqual([
360
+ {
361
+ id: "legacy_part",
362
+ message_id: "legacy_message",
363
+ session_id: "session",
364
+ data: '{"type":"text","text":"hello"}',
365
+ },
366
+ ])
367
+ expect(yield* db.all(sql`SELECT id FROM session_message`)).toEqual([])
368
+
369
+ yield* db.run(
370
+ sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('fresh_projection', 'session', 'user', 7, 2, 2, '{}')`,
371
+ )
372
+ expect(yield* db.get(sql`SELECT id, seq FROM session_message`)).toEqual({ id: "fresh_projection", seq: 7 })
373
+ }),
374
+ )
375
+ })
376
+
377
+ test("runs session usage backfill in order with schema changes", async () => {
378
+ await run(
379
+ Effect.gen(function* () {
380
+ const db = yield* makeDb
381
+ yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, time_updated integer NOT NULL)`)
382
+ yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, data text NOT NULL)`)
383
+ yield* db.run(sql`INSERT INTO session (id, time_updated) VALUES ('session_1', 1)`)
384
+ yield* db.run(
385
+ sql`INSERT INTO message (id, session_id, data) VALUES ('message_1', 'session_1', '{"role":"assistant","cost":1.25,"tokens":{"input":2,"output":3,"reasoning":4,"cache":{"read":5,"write":6}}}')`,
386
+ )
387
+
388
+ yield* DatabaseMigration.applyOnly(db, [sessionUsageMigration])
389
+
390
+ expect(
391
+ yield* db.get(
392
+ sql`SELECT cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write FROM session WHERE id = 'session_1'`,
393
+ ),
394
+ ).toEqual({
395
+ cost: 1.25,
396
+ tokens_input: 2,
397
+ tokens_output: 3,
398
+ tokens_reasoning: 4,
399
+ tokens_cache_read: 5,
400
+ tokens_cache_write: 6,
401
+ })
402
+ }),
403
+ )
404
+ })
405
+
406
+ test("normalizes Windows storage paths and leaves POSIX paths untouched", async () => {
407
+ await run(
408
+ Effect.gen(function* () {
409
+ const db = yield* makeDb
410
+ yield* db.run(sql`CREATE TABLE project (id text PRIMARY KEY, worktree text NOT NULL, sandboxes text NOT NULL)`)
411
+ yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, directory text NOT NULL, path text)`)
412
+ // Windows-shaped rows (drive + backslash) must be normalized.
413
+ yield* db.run(
414
+ sql`INSERT INTO project (id, worktree, sandboxes) VALUES (${"win"}, ${"C:\\Repo\\Thing"}, ${JSON.stringify([
415
+ "C:\\Repo\\Thing\\sandbox",
416
+ ])})`,
417
+ )
418
+ yield* db.run(
419
+ sql`INSERT INTO session (id, directory, path) VALUES (${"win"}, ${"C:\\Repo\\Thing\\packages\\api"}, ${"packages\\api"})`,
420
+ )
421
+ // UNC worktrees and their sandboxes must normalize too (not just drive paths).
422
+ yield* db.run(
423
+ sql`INSERT INTO project (id, worktree, sandboxes) VALUES (${"unc"}, ${"\\\\server\\share"}, ${JSON.stringify([
424
+ "\\\\server\\share\\sandbox",
425
+ ])})`,
426
+ )
427
+ // The "/" worktree sentinel and POSIX paths (including a pathological
428
+ // backslash in a POSIX filename) must survive byte-for-byte.
429
+ yield* db.run(sql`INSERT INTO project (id, worktree, sandboxes) VALUES (${"global"}, ${"/"}, ${"[]"})`)
430
+ yield* db.run(
431
+ sql`INSERT INTO session (id, directory, path) VALUES (${"posix"}, ${"/home/me/we\\ird"}, ${"src\\weird"})`,
432
+ )
433
+
434
+ yield* DatabaseMigration.applyOnly(db, [normalizeStoragePathsMigration])
435
+
436
+ expect(yield* db.get(sql`SELECT worktree, sandboxes FROM project WHERE id = 'win'`)).toEqual({
437
+ worktree: "C:/Repo/Thing",
438
+ sandboxes: JSON.stringify(["C:/Repo/Thing/sandbox"]),
439
+ })
440
+ expect(yield* db.get(sql`SELECT directory, path FROM session WHERE id = 'win'`)).toEqual({
441
+ directory: "C:/Repo/Thing/packages/api",
442
+ path: "packages/api",
443
+ })
444
+ expect(yield* db.get(sql`SELECT worktree, sandboxes FROM project WHERE id = 'unc'`)).toEqual({
445
+ worktree: "//server/share",
446
+ sandboxes: JSON.stringify(["//server/share/sandbox"]),
447
+ })
448
+ expect(yield* db.get(sql`SELECT worktree FROM project WHERE id = 'global'`)).toEqual({ worktree: "/" })
449
+ expect(yield* db.get(sql`SELECT directory, path FROM session WHERE id = 'posix'`)).toEqual({
450
+ directory: "/home/me/we\\ird",
451
+ path: "src\\weird",
452
+ })
453
+ }),
454
+ )
455
+ })
456
+
457
+ test("maps native Windows paths through database columns", async () => {
458
+ if (process.platform !== "win32") return
459
+ await run(
460
+ Effect.gen(function* () {
461
+ const db = yield* makeDb
462
+ yield* DatabaseMigration.apply(db)
463
+ const projectID = ProjectV2.ID.make("codec_project")
464
+ const worktree = AbsolutePath.make("C:\\Repo\\Thing")
465
+ const sandbox = AbsolutePath.make("C:\\Repo\\Thing\\sandbox")
466
+ const directory = "C:\\Repo\\Thing\\packages\\api"
467
+ const sessionID = SessionSchema.ID.make("ses_codec")
468
+
469
+ expect(() =>
470
+ Effect.runSync(
471
+ db
472
+ .insert(ProjectTable)
473
+ .values({
474
+ id: ProjectV2.ID.make("invalid_path"),
475
+ worktree: AbsolutePath.make("not-absolute"),
476
+ sandboxes: [],
477
+ time_created: 1,
478
+ time_updated: 1,
479
+ })
480
+ .run(),
481
+ ),
482
+ ).toThrow()
483
+
484
+ yield* db
485
+ .insert(ProjectTable)
486
+ .values({
487
+ id: projectID,
488
+ worktree,
489
+ sandboxes: [sandbox],
490
+ time_created: 1,
491
+ time_updated: 1,
492
+ })
493
+ .run()
494
+ yield* db
495
+ .insert(SessionTable)
496
+ .values({
497
+ id: sessionID,
498
+ project_id: projectID,
499
+ slug: "codec",
500
+ directory,
501
+ path: "packages\\api",
502
+ title: "Codec",
503
+ version: "test",
504
+ time_created: 1,
505
+ time_updated: 1,
506
+ })
507
+ .run()
508
+
509
+ expect(
510
+ yield* db.get<{ worktree: string; sandboxes: string }>(
511
+ sql`SELECT worktree, sandboxes FROM project WHERE id = ${projectID}`,
512
+ ),
513
+ ).toEqual({
514
+ worktree: "C:/Repo/Thing",
515
+ sandboxes: JSON.stringify(["C:/Repo/Thing/sandbox"]),
516
+ })
517
+ expect(
518
+ yield* db.get<{ directory: string; path: string }>(
519
+ sql`SELECT directory, path FROM session WHERE id = ${sessionID}`,
520
+ ),
521
+ ).toEqual({
522
+ directory: "C:/Repo/Thing/packages/api",
523
+ path: "packages/api",
524
+ })
525
+
526
+ const project = yield* db.select().from(ProjectTable).where(eq(ProjectTable.worktree, worktree)).get()
527
+ const session = yield* db.select().from(SessionTable).where(eq(SessionTable.directory, directory)).get()
528
+ expect(project?.worktree).toBe(worktree)
529
+ expect(project?.sandboxes).toEqual([sandbox])
530
+ expect(session?.directory).toBe(directory)
531
+ expect(session?.path).toBe("packages/api")
532
+
533
+ expect((yield* db.select().from(SessionTable).where(eq(SessionTable.path, "packages\\api")).get())?.id).toBe(
534
+ sessionID,
535
+ )
536
+
537
+ const moved = AbsolutePath.make("D:\\Moved\\Thing")
538
+ const updated = yield* db
539
+ .update(ProjectTable)
540
+ .set({ worktree: moved, sandboxes: [moved] })
541
+ .where(eq(ProjectTable.id, projectID))
542
+ .returning()
543
+ .get()
544
+ expect(updated?.worktree).toBe(moved)
545
+ expect(updated?.sandboxes).toEqual([moved])
546
+ expect(
547
+ yield* db.get<{ worktree: string; sandboxes: string }>(
548
+ sql`SELECT worktree, sandboxes FROM project WHERE id = ${projectID}`,
549
+ ),
550
+ ).toEqual({ worktree: "D:/Moved/Thing", sandboxes: JSON.stringify(["D:/Moved/Thing"]) })
551
+ expect(
552
+ (yield* db
553
+ .select()
554
+ .from(ProjectTable)
555
+ .where(inArray(ProjectTable.worktree, [moved]))
556
+ .get())?.id,
557
+ ).toBe(projectID)
558
+
559
+ yield* db.run(sql`UPDATE project SET worktree = ${"not-absolute"} WHERE id = ${projectID}`)
560
+ expect(() =>
561
+ Effect.runSync(db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()),
562
+ ).toThrow()
563
+ }),
564
+ )
565
+ })
566
+
567
+ test("imports existing drizzle migration state", async () => {
568
+ await run(
569
+ Effect.gen(function* () {
570
+ const db = yield* makeDb
571
+ yield* db.run(
572
+ sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
573
+ )
574
+ yield* db.run(sql`
575
+ INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
576
+ VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
577
+ `)
578
+
579
+ yield* DatabaseMigration.applyOnly(db, [])
580
+
581
+ expect(yield* db.get(sql`SELECT id FROM migration`)).toEqual({ id: "20260127222353_familiar_lady_ursula" })
582
+ }),
583
+ )
584
+ })
585
+
586
+ test("does not replay a migrated session metadata column", async () => {
587
+ await run(
588
+ Effect.gen(function* () {
589
+ const db = yield* makeDb
590
+ yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, metadata text)`)
591
+ yield* db.run(
592
+ sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
593
+ )
594
+ yield* db.run(sql`
595
+ INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
596
+ VALUES ('hash', 1, '20260511173437_session-metadata', ${new Date().toISOString()})
597
+ `)
598
+
599
+ yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration])
600
+
601
+ expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260511173437_session-metadata" }])
602
+ }),
603
+ )
604
+ })
605
+
606
+ test("accepts the temporary replacement session metadata migration id", async () => {
607
+ await run(
608
+ Effect.gen(function* () {
609
+ const db = yield* makeDb
610
+ yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, metadata text)`)
611
+ yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
612
+ yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('20260530232709_lovely_romulus', 1)`)
613
+
614
+ yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration])
615
+
616
+ expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([
617
+ { id: "20260511173437_session-metadata" },
618
+ { id: "20260530232709_lovely_romulus" },
619
+ ])
620
+ }),
621
+ )
622
+ })
623
+
624
+ test("skips drizzle import when migration table already has state", async () => {
625
+ await run(
626
+ Effect.gen(function* () {
627
+ const db = yield* makeDb
628
+ yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
629
+ yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('existing', 1)`)
630
+ yield* db.run(
631
+ sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
632
+ )
633
+ yield* db.run(sql`
634
+ INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
635
+ VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
636
+ `)
637
+
638
+ yield* DatabaseMigration.applyOnly(db, [])
639
+
640
+ expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([{ id: "existing" }])
641
+ }),
642
+ )
643
+ })
644
+ })