@neuralis/brain-core 0.1.0 → 0.2.0

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 (383) hide show
  1. package/README.md +26 -35
  2. package/app/cards/DiffView.tsx +9 -7
  3. package/app/cards/README.md +1 -1
  4. package/app/cards/diffLine.tsx +2 -4
  5. package/app/filesystem/README.md +1 -1
  6. package/app/filesystem/api/index.ts +14 -5
  7. package/app/filesystem/domain/types.ts +24 -5
  8. package/app/filesystem/store/filesystemStore.ts +20 -1
  9. package/app/filesystem/store/listSlice.ts +2 -0
  10. package/app/filesystem/store/searchSlice.ts +24 -1
  11. package/app/filesystem/ui/FileTreeSearch.tsx +29 -2
  12. package/app/filesystem/ui/PendingChangesPanel.tsx +74 -17
  13. package/app/filesystem/ui/PendingReviewDialog.tsx +45 -20
  14. package/app/filesystem/ui/README.md +8 -7
  15. package/app/filesystem/ui/ReembedDialog.tsx +20 -9
  16. package/app/filesystem/ui/SourceDeleteConfirm.tsx +1 -1
  17. package/app/filesystem/ui/SourcesPanel.tsx +110 -17
  18. package/app/filesystem/ui/VectorHealthPanel.tsx +14 -0
  19. package/app/filesystem/ui/pendingChangesModel.ts +9 -5
  20. package/dist/app/chunks/{astro-LRAFGXNX.js → astro-B2J4AS2D.js} +1 -1
  21. package/dist/app/chunks/bash-KY6SAQTH.js +1 -0
  22. package/dist/app/chunks/c-OBOPYVZ7.js +1 -0
  23. package/dist/app/chunks/{chunk-5H7ZCOD3.js → chunk-2X3GNKJ3.js} +1 -1
  24. package/dist/app/chunks/{chunk-XIFGTNRM.js → chunk-JPJIVRFI.js} +1 -1
  25. package/dist/app/chunks/{chunk-RH3QY5LF.js → chunk-LEFIFWND.js} +1 -1
  26. package/dist/app/chunks/{chunk-G4RZ3UNX.js → chunk-N6RXX3PH.js} +1 -1
  27. package/dist/app/chunks/{chunk-QXXJ73P5.js → chunk-PZAW4HPB.js} +1 -1
  28. package/dist/app/chunks/{chunk-3QLE7Y4L.js → chunk-ZBYWWTO6.js} +1 -1
  29. package/dist/app/chunks/cpp-TVWVOTEQ.js +1 -0
  30. package/dist/app/chunks/css-MANZBYA4.js +1 -0
  31. package/dist/app/chunks/{elixir-M36XROA6.js → elixir-S725Z7EH.js} +1 -1
  32. package/dist/app/chunks/{elm-7BG6QAIK.js → elm-5WUUSTNB.js} +1 -1
  33. package/dist/app/chunks/graphql-UGOPR3WU.js +1 -0
  34. package/dist/app/chunks/html-F6FWTBSG.js +1 -0
  35. package/dist/app/chunks/java-ANIVV5H5.js +1 -0
  36. package/dist/app/chunks/javascript-IH374YWG.js +1 -0
  37. package/dist/app/chunks/json-WGGVVXSB.js +1 -0
  38. package/dist/app/chunks/jsx-OTXJQD32.js +1 -0
  39. package/dist/app/chunks/{latex-ZWK7SXB6.js → latex-QCZNITGN.js} +1 -1
  40. package/dist/app/chunks/lua-4CRZ64PU.js +1 -0
  41. package/dist/app/chunks/{nginx-2L6VRV3A.js → nginx-V4CYS7WR.js} +1 -1
  42. package/dist/app/chunks/{perl-PBUAXXK6.js → perl-BHTX7F5B.js} +1 -1
  43. package/dist/app/chunks/{php-QJX57PA6.js → php-AYGJ5WYZ.js} +1 -1
  44. package/dist/app/chunks/r-4YFCFA4B.js +1 -0
  45. package/dist/app/chunks/{ruby-RLOWMVPO.js → ruby-42NMJWAL.js} +1 -1
  46. package/dist/app/chunks/{scss-B5CAI3HR.js → scss-XD7VU2CU.js} +1 -1
  47. package/dist/app/chunks/sql-B55MH6JJ.js +1 -0
  48. package/dist/app/chunks/{svelte-GT3D4C44.js → svelte-WR3QPIJU.js} +1 -1
  49. package/dist/app/chunks/tsx-S73GAE2L.js +1 -0
  50. package/dist/app/chunks/typescript-NEWOAB5Z.js +1 -0
  51. package/dist/app/chunks/{vue-JOW62MHD.js → vue-CCDS4VZK.js} +1 -1
  52. package/dist/app/chunks/xml-UXKAGLDR.js +1 -0
  53. package/dist/app/chunks/yaml-UMWKDTK7.js +1 -0
  54. package/dist/app/host.js +40 -40
  55. package/dist/scripts/embeddingLockV3.d.ts +30 -0
  56. package/dist/scripts/embeddingLockV3.d.ts.map +1 -0
  57. package/dist/scripts/embeddingLockV3.js +100 -0
  58. package/dist/scripts/embeddingLockV3.js.map +1 -0
  59. package/dist/scripts/vectorProofV1.d.ts +90 -0
  60. package/dist/scripts/vectorProofV1.d.ts.map +1 -0
  61. package/dist/scripts/vectorProofV1.js +220 -0
  62. package/dist/scripts/vectorProofV1.js.map +1 -0
  63. package/dist/src/config/env.d.ts +21 -0
  64. package/dist/src/config/env.d.ts.map +1 -1
  65. package/dist/src/config/env.js +55 -0
  66. package/dist/src/config/env.js.map +1 -1
  67. package/dist/src/connectors/AppSourceConfigStore.d.ts +12 -2
  68. package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
  69. package/dist/src/connectors/AppSourceConfigStore.js +48 -40
  70. package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
  71. package/dist/src/connectors/LocalDiskConnector.d.ts +11 -14
  72. package/dist/src/connectors/LocalDiskConnector.d.ts.map +1 -1
  73. package/dist/src/connectors/LocalDiskConnector.js +87 -107
  74. package/dist/src/connectors/LocalDiskConnector.js.map +1 -1
  75. package/dist/src/connectors/sourceConfigMutation.d.ts +11 -1
  76. package/dist/src/connectors/sourceConfigMutation.d.ts.map +1 -1
  77. package/dist/src/connectors/sourceConfigMutation.js +36 -3
  78. package/dist/src/connectors/sourceConfigMutation.js.map +1 -1
  79. package/dist/src/domain/chunker.js +8 -3
  80. package/dist/src/domain/chunker.js.map +1 -1
  81. package/dist/src/domain/healthTypes.d.ts +11 -0
  82. package/dist/src/domain/healthTypes.d.ts.map +1 -1
  83. package/dist/src/domain/mime.d.ts +1 -0
  84. package/dist/src/domain/mime.d.ts.map +1 -1
  85. package/dist/src/domain/mime.js +48 -0
  86. package/dist/src/domain/mime.js.map +1 -1
  87. package/dist/src/domain/pendingChange.d.ts +7 -0
  88. package/dist/src/domain/pendingChange.d.ts.map +1 -1
  89. package/dist/src/domain/pendingChange.js +11 -0
  90. package/dist/src/domain/pendingChange.js.map +1 -1
  91. package/dist/src/domain/sourceConfig.d.ts +7 -14
  92. package/dist/src/domain/sourceConfig.d.ts.map +1 -1
  93. package/dist/src/domain/sourceConfig.js +1 -13
  94. package/dist/src/domain/sourceConfig.js.map +1 -1
  95. package/dist/src/domain/sourcePackages.d.ts +11 -2
  96. package/dist/src/domain/sourcePackages.d.ts.map +1 -1
  97. package/dist/src/domain/syncTrigger.d.ts +2 -3
  98. package/dist/src/domain/syncTrigger.d.ts.map +1 -1
  99. package/dist/src/domain/syncTrigger.js +2 -3
  100. package/dist/src/domain/syncTrigger.js.map +1 -1
  101. package/dist/src/domain/syncTypes.d.ts +21 -4
  102. package/dist/src/domain/syncTypes.d.ts.map +1 -1
  103. package/dist/src/domain/syncTypes.js.map +1 -1
  104. package/dist/src/domain/text.d.ts +3 -0
  105. package/dist/src/domain/text.d.ts.map +1 -1
  106. package/dist/src/domain/text.js +5 -0
  107. package/dist/src/domain/text.js.map +1 -1
  108. package/dist/src/domain/types.d.ts +9 -0
  109. package/dist/src/domain/types.d.ts.map +1 -1
  110. package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
  111. package/dist/src/embeddings/clients/GeminiEmbedder.js +11 -5
  112. package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
  113. package/dist/src/embeddings/clients/OllamaEmbedder.d.ts.map +1 -1
  114. package/dist/src/embeddings/clients/OllamaEmbedder.js +13 -6
  115. package/dist/src/embeddings/clients/OllamaEmbedder.js.map +1 -1
  116. package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
  117. package/dist/src/embeddings/clients/OpenAIEmbedder.js +12 -9
  118. package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
  119. package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
  120. package/dist/src/embeddings/clients/QwenEmbedder.js +12 -5
  121. package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
  122. package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
  123. package/dist/src/embeddings/clients/VoyageEmbedder.js +12 -5
  124. package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
  125. package/dist/src/embeddings/factory.d.ts +2 -0
  126. package/dist/src/embeddings/factory.d.ts.map +1 -1
  127. package/dist/src/embeddings/factory.js +23 -3
  128. package/dist/src/embeddings/factory.js.map +1 -1
  129. package/dist/src/embeddings/lock.d.ts +22 -11
  130. package/dist/src/embeddings/lock.d.ts.map +1 -1
  131. package/dist/src/embeddings/lock.js +47 -29
  132. package/dist/src/embeddings/lock.js.map +1 -1
  133. package/dist/src/embeddings/retry.d.ts +5 -0
  134. package/dist/src/embeddings/retry.d.ts.map +1 -1
  135. package/dist/src/embeddings/retry.js +35 -0
  136. package/dist/src/embeddings/retry.js.map +1 -1
  137. package/dist/src/events/pending.d.ts +56 -0
  138. package/dist/src/events/pending.d.ts.map +1 -0
  139. package/dist/src/events/pending.js +108 -0
  140. package/dist/src/events/pending.js.map +1 -0
  141. package/dist/src/events/source.d.ts +60 -14
  142. package/dist/src/events/source.d.ts.map +1 -1
  143. package/dist/src/events/source.js +149 -31
  144. package/dist/src/events/source.js.map +1 -1
  145. package/dist/src/events/vector.d.ts +33 -0
  146. package/dist/src/events/vector.d.ts.map +1 -0
  147. package/dist/src/events/vector.js +57 -0
  148. package/dist/src/events/vector.js.map +1 -0
  149. package/dist/src/git/GitRunner.d.ts.map +1 -1
  150. package/dist/src/git/GitRunner.js +11 -8
  151. package/dist/src/git/GitRunner.js.map +1 -1
  152. package/dist/src/git/sourceGitControl.d.ts.map +1 -1
  153. package/dist/src/git/sourceGitControl.js +2 -1
  154. package/dist/src/git/sourceGitControl.js.map +1 -1
  155. package/dist/src/index.d.ts +5 -4
  156. package/dist/src/index.d.ts.map +1 -1
  157. package/dist/src/index.js +4 -3
  158. package/dist/src/index.js.map +1 -1
  159. package/dist/src/lifecycle.d.ts +20 -4
  160. package/dist/src/lifecycle.d.ts.map +1 -1
  161. package/dist/src/lifecycle.js +5 -1
  162. package/dist/src/lifecycle.js.map +1 -1
  163. package/dist/src/routes/health.js +6 -1
  164. package/dist/src/routes/health.js.map +1 -1
  165. package/dist/src/routes/pending.d.ts.map +1 -1
  166. package/dist/src/routes/pending.js +2 -1
  167. package/dist/src/routes/pending.js.map +1 -1
  168. package/dist/src/routes/sources.d.ts.map +1 -1
  169. package/dist/src/routes/sources.js +60 -11
  170. package/dist/src/routes/sources.js.map +1 -1
  171. package/dist/src/services/BridgeService.d.ts +2 -2
  172. package/dist/src/services/BridgeService.d.ts.map +1 -1
  173. package/dist/src/services/BridgeService.js +11 -2
  174. package/dist/src/services/BridgeService.js.map +1 -1
  175. package/dist/src/services/FilesystemService.d.ts +3 -1
  176. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  177. package/dist/src/services/FilesystemService.js +3 -1
  178. package/dist/src/services/FilesystemService.js.map +1 -1
  179. package/dist/src/services/FilesystemUiService.js +1 -1
  180. package/dist/src/services/FilesystemUiService.js.map +1 -1
  181. package/dist/src/services/SyncScheduler.d.ts.map +1 -1
  182. package/dist/src/services/SyncScheduler.js +0 -14
  183. package/dist/src/services/SyncScheduler.js.map +1 -1
  184. package/dist/src/services/VectorHealthService.d.ts.map +1 -1
  185. package/dist/src/services/VectorHealthService.js +2 -46
  186. package/dist/src/services/VectorHealthService.js.map +1 -1
  187. package/dist/src/services/baselineRepair.d.ts +2 -2
  188. package/dist/src/services/baselineRepair.js +2 -2
  189. package/dist/src/services/declaredSources.d.ts +8 -1
  190. package/dist/src/services/declaredSources.d.ts.map +1 -1
  191. package/dist/src/services/declaredSources.js +19 -19
  192. package/dist/src/services/declaredSources.js.map +1 -1
  193. package/dist/src/store/dataFormats.d.ts +14 -3
  194. package/dist/src/store/dataFormats.d.ts.map +1 -1
  195. package/dist/src/store/dataFormats.js +15 -5
  196. package/dist/src/store/dataFormats.js.map +1 -1
  197. package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
  198. package/dist/src/sync/DeltaSyncRunner.js +78 -243
  199. package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
  200. package/dist/src/sync/SyncJobQueue.d.ts +33 -4
  201. package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
  202. package/dist/src/sync/SyncJobQueue.js +179 -19
  203. package/dist/src/sync/SyncJobQueue.js.map +1 -1
  204. package/dist/src/sync/moveDetector.d.ts +2 -1
  205. package/dist/src/sync/moveDetector.d.ts.map +1 -1
  206. package/dist/src/sync/moveDetector.js +2 -1
  207. package/dist/src/sync/moveDetector.js.map +1 -1
  208. package/dist/src/tools/fs_search.d.ts.map +1 -1
  209. package/dist/src/tools/fs_search.js +8 -0
  210. package/dist/src/tools/fs_search.js.map +1 -1
  211. package/dist/src/tools/fs_write.js +3 -1
  212. package/dist/src/tools/fs_write.js.map +1 -1
  213. package/dist/src/usecases/chunkUpsert.d.ts +4 -0
  214. package/dist/src/usecases/chunkUpsert.d.ts.map +1 -1
  215. package/dist/src/usecases/chunkUpsert.js +103 -60
  216. package/dist/src/usecases/chunkUpsert.js.map +1 -1
  217. package/dist/src/usecases/embeddingBacklog.d.ts +31 -0
  218. package/dist/src/usecases/embeddingBacklog.d.ts.map +1 -0
  219. package/dist/src/usecases/embeddingBacklog.js +244 -0
  220. package/dist/src/usecases/embeddingBacklog.js.map +1 -0
  221. package/dist/src/usecases/listSourcePackages.js +17 -1
  222. package/dist/src/usecases/listSourcePackages.js.map +1 -1
  223. package/dist/src/usecases/mediaLane.d.ts +11 -4
  224. package/dist/src/usecases/mediaLane.d.ts.map +1 -1
  225. package/dist/src/usecases/mediaLane.js +67 -30
  226. package/dist/src/usecases/mediaLane.js.map +1 -1
  227. package/dist/src/usecases/pendingReview.d.ts.map +1 -1
  228. package/dist/src/usecases/pendingReview.js +24 -7
  229. package/dist/src/usecases/pendingReview.js.map +1 -1
  230. package/dist/src/usecases/purgeExcludedPoints.d.ts +2 -0
  231. package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
  232. package/dist/src/usecases/purgeExcludedPoints.js +3 -2
  233. package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
  234. package/dist/src/usecases/readFiles.d.ts.map +1 -1
  235. package/dist/src/usecases/readFiles.js +33 -6
  236. package/dist/src/usecases/readFiles.js.map +1 -1
  237. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +31 -0
  238. package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -0
  239. package/dist/src/usecases/reconcileSourcePackageRoots.js +114 -0
  240. package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -0
  241. package/dist/src/usecases/searchFiles.d.ts.map +1 -1
  242. package/dist/src/usecases/searchFiles.js +62 -64
  243. package/dist/src/usecases/searchFiles.js.map +1 -1
  244. package/dist/src/usecases/syncIndexedFile.d.ts +42 -0
  245. package/dist/src/usecases/syncIndexedFile.d.ts.map +1 -0
  246. package/dist/src/usecases/syncIndexedFile.js +242 -0
  247. package/dist/src/usecases/syncIndexedFile.js.map +1 -0
  248. package/dist/src/usecases/syncPointWrites.d.ts +5 -0
  249. package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
  250. package/dist/src/usecases/syncPointWrites.js +32 -5
  251. package/dist/src/usecases/syncPointWrites.js.map +1 -1
  252. package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
  253. package/dist/src/usecases/uploadBinary.js +36 -49
  254. package/dist/src/usecases/uploadBinary.js.map +1 -1
  255. package/dist/src/usecases/vector/endpointGuard.js +2 -1
  256. package/dist/src/usecases/vector/endpointGuard.js.map +1 -1
  257. package/dist/src/usecases/vector/estimate.d.ts +7 -4
  258. package/dist/src/usecases/vector/estimate.d.ts.map +1 -1
  259. package/dist/src/usecases/vector/estimate.js +8 -5
  260. package/dist/src/usecases/vector/estimate.js.map +1 -1
  261. package/dist/src/usecases/vector/rebuildGeneration.d.ts +29 -5
  262. package/dist/src/usecases/vector/rebuildGeneration.d.ts.map +1 -1
  263. package/dist/src/usecases/vector/rebuildGeneration.js +224 -82
  264. package/dist/src/usecases/vector/rebuildGeneration.js.map +1 -1
  265. package/dist/src/usecases/vectorMaintenance.d.ts +46 -4
  266. package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
  267. package/dist/src/usecases/vectorMaintenance.js +190 -11
  268. package/dist/src/usecases/vectorMaintenance.js.map +1 -1
  269. package/dist/src/usecases/write/commit.d.ts +6 -8
  270. package/dist/src/usecases/write/commit.d.ts.map +1 -1
  271. package/dist/src/usecases/write/commit.js +96 -112
  272. package/dist/src/usecases/write/commit.js.map +1 -1
  273. package/dist/src/usecases/write/content.js +1 -2
  274. package/dist/src/usecases/write/content.js.map +1 -1
  275. package/dist/src/usecases/write/ports.d.ts +1 -0
  276. package/dist/src/usecases/write/ports.d.ts.map +1 -1
  277. package/dist/src/vector/FilesystemVectorIndex.d.ts +34 -3
  278. package/dist/src/vector/FilesystemVectorIndex.d.ts.map +1 -1
  279. package/dist/src/vector/FilesystemVectorIndex.js +254 -20
  280. package/dist/src/vector/FilesystemVectorIndex.js.map +1 -1
  281. package/dist/src/vector/InMemoryVectorIndex.d.ts +9 -2
  282. package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
  283. package/dist/src/vector/InMemoryVectorIndex.js +25 -6
  284. package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
  285. package/dist/src/vector/QdrantVectorIndex.d.ts +35 -9
  286. package/dist/src/vector/QdrantVectorIndex.d.ts.map +1 -1
  287. package/dist/src/vector/QdrantVectorIndex.js +209 -90
  288. package/dist/src/vector/QdrantVectorIndex.js.map +1 -1
  289. package/dist/src/vector/qdrantFilter.d.ts +3 -1
  290. package/dist/src/vector/qdrantFilter.d.ts.map +1 -1
  291. package/dist/src/vector/qdrantFilter.js +2 -0
  292. package/dist/src/vector/qdrantFilter.js.map +1 -1
  293. package/dist/src/vector/types.d.ts +81 -0
  294. package/dist/src/vector/types.d.ts.map +1 -1
  295. package/dist/src/vector/types.js +2 -0
  296. package/dist/src/vector/types.js.map +1 -1
  297. package/dist/src/wiring.d.ts +6 -0
  298. package/dist/src/wiring.d.ts.map +1 -1
  299. package/dist/src/wiring.js +62 -3
  300. package/dist/src/wiring.js.map +1 -1
  301. package/dist/tools/fs_search.json +2 -2
  302. package/docs/drive-overview.md +2 -2
  303. package/instructions/brain-model.md +1 -1
  304. package/package.json +999 -10
  305. package/rules/brain-rules.md +1 -1
  306. package/skills/connect-business-data/SKILL.md +6 -4
  307. package/skills/create-package/SKILL.md +14 -12
  308. package/skills/create-package/references/config-and-credentials.md +3 -3
  309. package/skills/create-package/references/connectors-and-uri-policies.md +5 -3
  310. package/skills/create-package/references/manifest-and-discovery.md +4 -3
  311. package/skills/create-package/references/runtime-and-build.md +9 -9
  312. package/skills/create-package/references/skills-and-context.md +3 -3
  313. package/skills/create-package/references/surfaces-and-design.md +2 -1
  314. package/skills/create-package/references/team-and-workflows.md +2 -2
  315. package/skills/create-package/references/test-and-ship.md +15 -15
  316. package/skills/create-package/references/tools-and-schemas.md +1 -1
  317. package/skills/create-package/scripts/check-package.sh +3 -3
  318. package/skills/create-package/scripts/verify-loaded.sh +16 -16
  319. package/skills/manage-files/SKILL.md +2 -2
  320. package/skills/manage-sources/SKILL.md +16 -15
  321. package/skills/manage-sources/scripts/reembed.sh +9 -3
  322. package/skills/sync/SKILL.md +4 -4
  323. package/tools/fs_search.json +2 -2
  324. package/dist/app/chunks/bash-M45GHPBQ.js +0 -1
  325. package/dist/app/chunks/c-X3REOAWD.js +0 -1
  326. package/dist/app/chunks/cpp-VOWI743W.js +0 -1
  327. package/dist/app/chunks/css-2ZVPHE25.js +0 -1
  328. package/dist/app/chunks/graphql-7V2NP54C.js +0 -1
  329. package/dist/app/chunks/html-UF3LFLNA.js +0 -1
  330. package/dist/app/chunks/java-PNTAU4L3.js +0 -1
  331. package/dist/app/chunks/javascript-4JJTQGEL.js +0 -1
  332. package/dist/app/chunks/json-VLQ6D3JH.js +0 -1
  333. package/dist/app/chunks/jsx-KQKSLAAH.js +0 -1
  334. package/dist/app/chunks/lua-UFPYUJ2O.js +0 -1
  335. package/dist/app/chunks/r-QEZB45SR.js +0 -1
  336. package/dist/app/chunks/sql-DTZHNUZP.js +0 -1
  337. package/dist/app/chunks/tsx-WPR2WRZD.js +0 -1
  338. package/dist/app/chunks/typescript-LWO362OA.js +0 -1
  339. package/dist/app/chunks/xml-A5H72RTS.js +0 -1
  340. package/dist/app/chunks/yaml-MXHSIJCI.js +0 -1
  341. package/dist/src/services/purgePendingNodes.d.ts +0 -32
  342. package/dist/src/services/purgePendingNodes.d.ts.map +0 -1
  343. package/dist/src/services/purgePendingNodes.js +0 -61
  344. package/dist/src/services/purgePendingNodes.js.map +0 -1
  345. /package/dist/app/chunks/{chunk-737Y24HS.js → chunk-3JASFUPF.js} +0 -0
  346. /package/dist/app/chunks/{chunk-DKD2L57N.js → chunk-6EYLOQ4F.js} +0 -0
  347. /package/dist/app/chunks/{chunk-KYH5EHHY.js → chunk-7YRZ5LOQ.js} +0 -0
  348. /package/dist/app/chunks/{chunk-6TC3DVWV.js → chunk-C7LU4RID.js} +0 -0
  349. /package/dist/app/chunks/{chunk-TY33BMKR.js → chunk-CD52YDZP.js} +0 -0
  350. /package/dist/app/chunks/{chunk-OLEIKJ3X.js → chunk-DCJKTCJ5.js} +0 -0
  351. /package/dist/app/chunks/{chunk-QLBZ4JR2.js → chunk-EXIU2KBV.js} +0 -0
  352. /package/dist/app/chunks/{chunk-SFUNBBEA.js → chunk-FDP4LO4P.js} +0 -0
  353. /package/dist/app/chunks/{chunk-BGRVFATT.js → chunk-HNETUS7U.js} +0 -0
  354. /package/dist/app/chunks/{chunk-4QNMHAIR.js → chunk-LBFZJEDR.js} +0 -0
  355. /package/dist/app/chunks/{chunk-TRRUHDPN.js → chunk-ULRXL5VM.js} +0 -0
  356. /package/dist/app/chunks/{chunk-S6B4WWIF.js → chunk-XJK47SMJ.js} +0 -0
  357. /package/dist/app/chunks/{chunk-DZ5C5FKI.js → chunk-YYAAIAU2.js} +0 -0
  358. /package/dist/app/chunks/{clojure-OC6KPSJO.js → clojure-4QCN4GHG.js} +0 -0
  359. /package/dist/app/chunks/{core-KGE7TQJ3.js → core-ZHTX3P2Q.js} +0 -0
  360. /package/dist/app/chunks/{csharp-E7VIFITL.js → csharp-HQWR4N52.js} +0 -0
  361. /package/dist/app/chunks/{dark-plus-BOSCZXE4.js → dark-plus-GYPKKZES.js} +0 -0
  362. /package/dist/app/chunks/{dart-6Y3X27SZ.js → dart-FO4TTVO2.js} +0 -0
  363. /package/dist/app/chunks/{diff-TR2NPA64.js → diff-SHANV4YC.js} +0 -0
  364. /package/dist/app/chunks/{dist-T32U2VVR.js → dist-WSDF34DM.js} +0 -0
  365. /package/dist/app/chunks/{docker-N33HOW54.js → docker-VX55HTDS.js} +0 -0
  366. /package/dist/app/chunks/{go-6DGG5UTP.js → go-5UCPEC6S.js} +0 -0
  367. /package/dist/app/chunks/{haskell-KK36PMMM.js → haskell-25PJ3OSG.js} +0 -0
  368. /package/dist/app/chunks/{ini-VHVQ5TPB.js → ini-BJIVD5MK.js} +0 -0
  369. /package/dist/app/chunks/{kotlin-44WS3JHE.js → kotlin-MNR6XOEQ.js} +0 -0
  370. /package/dist/app/chunks/{less-5WZ3KLAK.js → less-KA5JURX6.js} +0 -0
  371. /package/dist/app/chunks/{light-plus-HGGBC43G.js → light-plus-QXH2UUWZ.js} +0 -0
  372. /package/dist/app/chunks/{make-X7RXMVW7.js → make-LPD5KOCO.js} +0 -0
  373. /package/dist/app/chunks/{markdown-46IKUAIB.js → markdown-LMWF6MQR.js} +0 -0
  374. /package/dist/app/chunks/{mdx-DGLHDAO7.js → mdx-WPMNN5BS.js} +0 -0
  375. /package/dist/app/chunks/{nix-Q6FYQZLX.js → nix-QGWLUUMZ.js} +0 -0
  376. /package/dist/app/chunks/{powershell-L4KJJ6DK.js → powershell-7GSBBEFF.js} +0 -0
  377. /package/dist/app/chunks/{prisma-IKS4LVH2.js → prisma-FPMPQISO.js} +0 -0
  378. /package/dist/app/chunks/{proto-AYCDOTJO.js → proto-7KDX5Z2U.js} +0 -0
  379. /package/dist/app/chunks/{python-KXMCSAFQ.js → python-CFG4FTQW.js} +0 -0
  380. /package/dist/app/chunks/{rust-J3BC3P6V.js → rust-X7A37WU3.js} +0 -0
  381. /package/dist/app/chunks/{scala-AYIIHBJK.js → scala-OYHJSUG2.js} +0 -0
  382. /package/dist/app/chunks/{swift-QPHHCQD3.js → swift-PE7VNYW5.js} +0 -0
  383. /package/dist/app/chunks/{toml-UV7JK7K7.js → toml-ETQRDILP.js} +0 -0
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @neuralis/brain-core
2
2
 
3
- The brain of Neuralis: any system a connector can reach — local directories, repositories, machine sandboxes, the vector memory itself — joins ONE URI-addressed, vector-indexed, project/user/agent-scoped virtual filesystem, chunked and embedded on sync, revertible on write, policy-governed per path. A synced source pays back twice: its content becomes searchable memory, and its markdown becomes source packages. Concretely: URI-native filesystem, vector memory, source connectors, and the Files UI.
3
+ The brain of Neuralis: any system a connector can reach — local directories, repositories, machine sandboxes, the vector memory itself — joins ONE URI-addressed, vector-indexed, project/user/agent-scoped virtual filesystem, chunked and indexed on sync with optional embedding, revertible on write, policy-governed per path. A synced source pays back twice: its content becomes searchable memory, and its markdown becomes source packages. Concretely: URI-native filesystem, vector memory, source connectors, and the Files UI.
4
4
 
5
5
  ## Responsibility
6
6
 
7
7
  - Read, create, modify, list, and search files across every registered source through one URI vocabulary.
8
8
  - Manage project-, user-, and agent-scoped source configurations and persist them in the platform zone.
9
9
  - Host the connector registry contract: source connectors declared by other packages plug in via `SourceKindRegistry`.
10
- - Sync connector content into a Qdrant-backed vector index with chunked embeddings.
10
+ - Sync connector content into a Qdrant-backed payload/full-text index, with optional chunked embeddings.
11
11
  - Expose the Filesystem package routes (`/api/packages/brain-core/*`) and the agent tools (`fs_read`, `fs_write`, `fs_list`, `fs_search`).
12
12
  - Render the workspace Files widget and the `filesystem.change` agent-card.
13
13
 
@@ -123,10 +123,12 @@ soft — no badges); (2) reject leading-dash refs/paths (option-injection)
123
123
  and require `--` before path operands; (3) `-c core.hooksPath=/dev/null
124
124
  -c core.fsmonitor=false -c core.pager=cat` on every call, defeating a malicious
125
125
  repo-local `.git/config` (closes the malicious-checkout RCE class); (4) strip EVERY
126
- ambient `GIT_*` env var and force `GIT_CONFIG_GLOBAL`/`SYSTEM=/dev/null`. Setting
127
- floors (3)/(4) defensively requires simple-git's explicit `unsafe:{…}` opt-ins,
128
- since simple-git blocks those same flags/env as attacker-injection vectors — which
129
- is why stripping ALL `GIT_*` (not a denylist) is the robust rule. It exports the
126
+ ambient `GIT_*` env var, then set `GIT_CONFIG_GLOBAL=/dev/null`,
127
+ `GIT_CONFIG_SYSTEM=/dev/null`, `GIT_TERMINAL_PROMPT=0` and `GIT_OPTIONAL_LOCKS=0`.
128
+ Simple-git v4 requires top-level `allowEnvironment` naming exactly those four
129
+ fixed defensive overrides, together with the existing explicit `unsafe:{…}`
130
+ opt-ins for floors (3)/(4). It blocks those same flags/env as attacker-injection
131
+ vectors — stripping ALL ambient `GIT_*` (not a denylist) remains the robust rule. It exports the
130
132
  pure helpers `classifyGitOp(op, args) → 'read'|'mutating'|'destructive'` (for the
131
133
  later approval gate) and `normalizeStatus`. Read primitives are fully implemented;
132
134
  local-mutating ops are implemented but NOT yet tool-exposed (they wait on the
@@ -687,8 +689,8 @@ through the ordinary user/agent-equality filter, unchanged.
687
689
  Lifecycle exports (`src/lifecycle.ts`):
688
690
 
689
691
  - `init`, `start`, `onBootstrapComplete`, `drain`, `stop`, `api`, `onCredentialChanged`, `health`, `onPrincipalRevoked`, `provisionProject` (`usecases/provisionProject.ts` — a NEW project's declared `seed:'auto'` sources, written complete through the same create path as the boot repair; see Declared sources), `deprovisionProject` (`usecases/deprovisionProject.ts` — on a PERMANENT project delete, ALL of the project's vector points leave the shared index in ONE bounded `deleteByFilter({ must: [{ field: 'projectId', equals: id }] })` — chunks + folder-ancestor points covered by the same keyword-index filter. MUST-HAVE: a failed delete throws, and so does a degraded boot where Qdrant is configured but the in-memory index is wired (a delete there would leave every real point in Qdrant); the host then stops the purge before any file is removed. It runs BEFORE the fs tree is removed, so resolving the brain infra cannot re-materialize the project dir).
690
- - `init` state exposes `services["package-source-roots"]` for kernel registration; its delegate is the same function as the direct API method.
691
- - `BrainCoreApi` — `getProjectServices`, `getProjectInfra`, `getFilesystem`, `upsertSourceConfig`, `listShellSourceRoots(projectId)` (every shell-capable source with its persisted permissions; feeds agent-core's shell uri-policy gate and the terminal's host-tab resolver; NOT access-filtered — consumers evaluate the policy against the caller's own `SessionContext`. Rows carry an explicit **`plane`**: `container` sources contribute their `containerRoot`, resolved by the canonical `resolveLocalRoot`; **host-plane** sources contribute a raw `hostRoot` and an intentionally EMPTY `containerRoot` — no container path exists for them, and the empty value is load-bearing, because the consumer's container-plane filter drops rootless entries so a host source can never leak into a container-plane lookup even if `plane` were ignored. The plane is derived from the connector KIND, never from the slug string — an ordinary local source may legitimately be named `host-notes`. Kinds that host no shell at all (brain vectors, webtop) contribute nothing), and `listSourceRuntimeSummaries(projectId, { userId, agentId, role?, grantedFeatures? })` (the READ sibling of `listShellSourceRoots`, feeding agent-core's `<runtime_stack>` source rendering — **all** connector kinds, **access-filtered** (host-plane rows carry `containerDir: null` — their root is a path in the OPERATOR's filesystem with no container equivalent, and the local-shaped derivation would otherwise print a container path that does not exist, baked into every agent's cached system prompt) via `canAccessScope` → `filesystem.observeScoped`/deny-by-default, each row carrying connector, `os_uri`/`container_dir`, the caller-effective uri-policy, `description`, and the cached index snapshot; reads persisted source-config JSON ONLY — no live walk / no vector count — so it is safe to call once per stream. Agent-core renders the stable structure in the cached `<runtime_stack>` prefix and the volatile counts + on/off in `<workspace_live>`. Disabled sources are INCLUDED, marked `enabledEffective:false`). `listPackageSourceRoots(projectId)` (enumerate every project-associated source as a runtime-WASM package-source candidate `{ slug, uriRoot, containerRoot, scope, recognizesPackages, enabled, kind }`; identity-free like `listShellSourceRoots`, ALL candidates returned so the host applies the floor — project-scope + flagged + enabled + local; the host's `projectPackages` consumes it to scan + hot-reload flagged source dirs alongside `_packages/`). Filesystem events reach the `/brain-core/events` SSE route and the discovered `filesystem` channel of the host `/api/events` hub through ONE authorization adapter: it takes the real session, authorizes EACH requested agent deny-by-default (a foreign agent yields zero events, never an existence-confirming error), and filters events by user/project/types and the authorized agent or source.
692
+ - `init` state exposes `services["package-source-roots"]` for kernel registration; its `listPackageSourceRoots` delegate is the same function as the direct API method, and its `subscribePackageSourceRoots(listener)` calls `listener(projectId)` after every committed source-config write of the app root (`SourceConfigMutation.onChanged` — fired outside the write lock, a no-op write never fires, a throwing listener is logged and never reaches the writer), so the host re-derives that project's flagged package roots at once.
693
+ - `BrainCoreApi` — `getProjectServices`, `getProjectInfra`, `getFilesystem`, `upsertSourceConfig`, `listShellSourceRoots(projectId)` (every shell-capable source with its persisted permissions; feeds agent-core's shell uri-policy gate and the terminal's host-tab resolver; NOT access-filtered — consumers evaluate the policy against the caller's own `SessionContext`. Rows carry an explicit **`plane`**: `container` sources contribute their `containerRoot`, resolved by the canonical `resolveLocalRoot`; **host-plane** sources contribute a raw `hostRoot` and an intentionally EMPTY `containerRoot` — no container path exists for them, and the empty value is load-bearing, because the consumer's container-plane filter drops rootless entries so a host source can never leak into a container-plane lookup even if `plane` were ignored. The plane is derived from the connector KIND, never from the slug string — an ordinary local source may legitimately be named `host-notes`. Kinds that host no shell at all (brain vectors, webtop) contribute nothing), and `listSourceRuntimeSummaries(projectId, { userId, agentId, role?, grantedFeatures? })` (the READ sibling of `listShellSourceRoots`, feeding agent-core's `<runtime_stack>` source rendering — **all** connector kinds, **access-filtered** (host-plane rows carry `containerDir: null` — their root is a path in the OPERATOR's filesystem with no container equivalent, and the local-shaped derivation would otherwise print a container path that does not exist, baked into every agent's cached system prompt) via `canAccessScope` → `filesystem.observeScoped`/deny-by-default, each row carrying connector, `os_uri`/`container_dir`, the caller-effective uri-policy, `description`, and the cached index snapshot; reads persisted source-config JSON ONLY — no live walk / no vector count — so it is safe to call once per stream. Agent-core renders the stable structure in the cached `<runtime_stack>` prefix and the volatile counts + on/off in `<workspace_live>`. Disabled sources are INCLUDED, marked `enabledEffective:false`). `listPackageSourceRoots(projectId)` (enumerate every project-associated source as a runtime package-source candidate `{ slug, uriRoot, containerRoot, scope, recognizesPackages, enabled, kind }`; identity-free like `listShellSourceRoots`, ALL candidates returned so the host applies the floor — project-scope + flagged + enabled + local; the host's `projectPackages` consumes it to scan + hot-reload flagged source dirs alongside `_packages/`). Filesystem events reach the `/brain-core/events` SSE route and the discovered `filesystem` channel of the host `/api/events` hub through ONE authorization adapter: it takes the real session, authorizes EACH requested agent deny-by-default (a foreign agent yields zero events, never an existence-confirming error), and filters events by user/project/types and the authorized agent or source.
692
694
 
693
695
  **Cross-package writes go through `FilesystemService` itself, and its permission check is
694
696
  the ONLY gate on that path.** `getProjectServices(projectId).filesystem` hands back the live
@@ -806,10 +808,9 @@ whole index: `enrichment_revision` (`src/sync/categoryInference.ts`) marks a pay
806
808
  derived fields are what the current rules give for its own uri + content, a stamped point
807
809
  is skipped before anything reads its content back, and only the unstamped candidates are
808
810
  hydrated — payload-only upserts, existing vector reused, no re-embed. On a settled source
809
- the sweep WRITES nothing. A point holding no inline text is HEALED there: an indexable uri
810
- whose file still hashes to the point's `content_hash` gets that text back, payload-only; one
811
- that cannot be healed never becomes stamped, so it stays a candidate and is re-hydrated each
812
- full pass, but it is never re-written. The
811
+ the sweep WRITES nothing. A point holding no inline text (a media point) never becomes
812
+ stamped, so it stays a candidate and is re-hydrated each full pass, but it is never
813
+ re-written unless a URI-derived field drifted. The
813
814
  stamp is written by ONE boundary,
814
815
  `src/vector/FilesystemVectorIndex.ts`, on every index write from every path; a payload it
815
816
  cannot derive from (a chunk, a tombstone, a `pending_review` draft, a node holding no
@@ -920,11 +921,11 @@ The embedding model registry (`src/embeddings/models.ts`, `EMBEDDING_MODELS`) is
920
921
 
921
922
  Every cloud provider has its OWN platform credential (`embedding.<provider>`), never the chat provider's `llm.*` key — so an embed call is counted by the credential-use rules and priced by the embedding spend meter on its own; an operator endpoint's key is `embedding.endpoint.<endpointId>`. Every call to a public provider or endpoint is address-checked and IP-pinned by the kernel's SSRF floor on every call. Many rows expose MRL / Matryoshka selectable dimensions (`supportedDimensions`).
922
923
 
923
- **Active vs target, and the switch.** The embedding lock records the ACTIVE generation (model, dimension, recipe, collection): every write and every search embeds with it, so one collection never mixes vector spaces. The configured `embeddingModelId`/`embeddingDimension` are the TARGET; a target that differs is a pending switch the admin Vector section shows with a sampled estimate — nothing switches by itself. The switch is a generation rebuild (`health/collection` `rebuild-generation`): the target re-embeds eligible points from their stored text into a new collection while the old one keeps serving, every write that lands meanwhile is mirrored onto it, and the swap moves the lock one generation on — `brain://` content and version history survive, and the dimension may change freely. It can be cancelled (the shadow is dropped, nothing else changes) and resumed after a restart (a restart pauses it). Separately, `sources/reindex` with `force` re-embeds a file, a folder or a source with the ACTIVE model, estimate first — an unchanged file keeps its payload (`updatedAt`, version history), so a re-embed never reads as an edit. Already recovery-only points with unavailable input retain their complete payload without vectors in the target; verified available input restores ordinary embedding. Unmarked missing input still pauses the rebuild. Recovery retention, cleanup and generation eligibility follow the constraints under Manifest-baseline merge below.
924
+ **Active vs target, and the switch.** The v3 serving lock records metadata mode or a certified ACTIVE dense generation (model, dimension, recipe, collection). `embeddingEnabled:false` makes payload sync/writes and keyword/filter search provider-free; semantic requests fall back with `requested_mode`, `effective_mode` and `embedding_degraded`. Result rows expose current/stale/missing/unknown embedding status, so old vectors never imply fresh content. Existing no-lock collections preserve unknown vectors in metadata mode without inferring a model. Dense work uses the certified active generation. The configured `embeddingModelId`/`embeddingDimension` are the TARGET; a target that differs is a pending switch the admin Vector section shows with a sampled estimate — nothing switches by itself. The switch is a generation rebuild (`health/collection` `rebuild-generation`): the target re-embeds eligible points from their stored text into a new collection while the old one keeps serving, every write that lands meanwhile is mirrored onto it, and the swap moves the lock one generation on — `brain://` content and version history survive, and the dimension may change freely. It can be cancelled (the shadow is dropped, nothing else changes) and resumed after a restart (a restart pauses it). Separately, `sources/reindex` with `force` re-embeds a file, a folder or a source with the ACTIVE model, estimate first — an unchanged file keeps its payload (`updatedAt`, version history), so a re-embed never reads as an edit. Already recovery-only points with unavailable input retain their complete payload without vectors in the target; verified available input restores ordinary embedding. Unmarked missing input still pauses the rebuild. Recovery retention, cleanup and generation eligibility follow the constraints under Manifest-baseline merge below.
924
925
 
925
- **Media — images, PDFs, video, audio.** With a multimodal ACTIVE model (`gemini-embedding-2` or `voyage-multimodal-3.5` — the registry `modalities` as far as the model's client can send them: an operator endpoint speaks the text-only `/v1/embeddings` wire, so its row's declared media modalities are kept but not embedded) the sync embeds media files from their BYTES: an image is one point, a PDF is embedded page by page through the connector's PDF renderer (the document point carries page 1, pages 2..`brainSyncMaxPdfPages` are page chunks), each point marked `modality` and carrying no stored bytes — a rebuild or a forced re-embed reads the source again. Each source chooses the kinds in its sync settings (`sync.media`): images and PDFs on, video and audio off by default; `brainSyncMaxMediaMb` bounds one file, and a kind switched off is purged by the next full pass. A text-only active model embeds no media file, and the source's last-sync line says how many it left out. Invoked PNG inputs receive bounded structural signature/chunk/CRC checks before provider work; an invalid skip preserves existing indexed and recovery payloads and is counted separately. This preflight does not decode pixels. A semantic search returns media hits with their `modality` (and a PDF's `page`); full-text search never matches them, they carry no text.
926
+ **Media — images, PDFs, video, audio.** With a multimodal ACTIVE model (`gemini-embedding-2` or `voyage-multimodal-3.5` — the registry `modalities` as far as the model's client can send them: an operator endpoint speaks the text-only `/v1/embeddings` wire, so its row's declared media modalities are kept but not embedded) the sync embeds media files from their BYTES: an image is one point, a PDF is embedded page by page through the connector's PDF renderer (the document point carries page 1, pages 2..`brainSyncMaxPdfPages` are page chunks), each point marked `modality` and carrying no stored bytes — a rebuild or a forced re-embed reads the source again. Each source chooses the kinds in its sync settings (`sync.media`): images and PDFs on, video and audio off by default; `brainSyncMaxMediaMb` bounds one file, and a kind switched off is purged by the next full pass. A text-only model or disabled embedding still indexes enabled media metadata and PDF page inventory without provider calls, reporting embedding debt; it never invents OCR text or semantic vectors. Invoked PNG inputs receive bounded structural signature/chunk/CRC checks before provider work; an invalid skip preserves existing indexed and recovery payloads and is counted separately. This preflight does not decode pixels. A semantic search returns media hits with their `modality` (and a PDF's `page`); full-text search never matches them, they carry no text.
926
927
 
927
- **Embedding spend.** Every priced or keyed embed call leaves one row in `<appRoot>/data/brain-core/embedding-usage.jsonl` (`{ts, model, tokens, estUsd, calls}`; an unpriced model is counted, never priced) and adds to today's aggregate the Vector section shows; `embeddingDailyUsdCap` (0 = off) stops embedding for the rest of the UTC day once reached — sync pauses with one resumable error, semantic search falls back to full-text.
928
+ **Embedding spend.** Every priced or keyed embed call leaves one row in `<appRoot>/data/brain-core/embedding-usage.jsonl` (`{ts, model, tokens, estUsd, calls}`; an unpriced model is counted, never priced) and adds to today's aggregate the Vector section shows; `embeddingDailyUsdCap` (0 = off) stops embedding for the rest of the UTC day once reached — payload sync continues with embedding debt and stops later provider calls for that job; semantic search falls back to full-text.
928
929
 
929
930
  **Credential-use limits (v1).** Embedding spend is governed by the platform's per-credential
930
931
  calls/period rules through `embeddings/usageGate.ts`: brain-core's `init` stashes the
@@ -934,13 +935,12 @@ batch that splits into several posts counts one use per post. This per-call hook
934
935
  REAL enforcement for the embed class (not telemetry): the key is construction-captured and
935
936
  `LazyEmbedder` memoizes the client, so the host's resolve-time counter sees roughly one
936
937
  grant per process. A deny throws the typed `CredentialUseLimitedError`
937
- (`code: 'credential_use_limited'`): **sync** breaks its per-file loop with one resumable
938
- `CREDENTIAL_USE_LIMITED` job error (the scheduler re-enqueues next tick; content-hash skip
939
- makes the retry cheap; there is no backoff — a denied source fail-fasts every tick),
938
+ (`code: 'credential_use_limited'`): **sync** continues payload indexing and records
939
+ `embeddingDegraded`; later provider calls in that job stop, while already running calls finish,
940
940
  **semantic search** soft-degrades to the full-text filter search with a `warning` on the
941
941
  response envelope — as it does for the embedding daily cap, a missing `embedding.*` key
942
942
  (`EmbeddingCredentialMissingError`) and an active model the platform no longer defines;
943
- every OTHER embed error keeps its propagate-don't-mask behavior.
943
+ Typed provider/network/timeout/quota unavailability degrades; validation, parse and local authorization errors propagate before the payload lands, storage errors propagate, and after the durable payload every embedding error — typed or not — is named debt in the answer (`usage.metadata.embeddingDeferred`), never a failed write. A sync against an out-of-credit embedder COMPLETES with `lastSyncResult.embeddingDegraded` (at most the concurrency's worth of provider calls, then no more that job) and writes its checkpoint, so the source is not due again until its own interval; only the embedding-only pass pauses, with the reason.
944
944
 
945
945
  since 2026-08-18 the search degrade covers a SECOND typed code:
946
946
  `ProviderQuotaExhaustedError` (`code: 'provider_quota_exhausted'`,
@@ -950,7 +950,7 @@ thrown immediately instead of burning the retry budget on a wait that cannot hel
950
950
  semantic search answers from full-text rather than a 500. The classification **fails toward
951
951
  retryable** — an unparseable or unrecognised body keeps the old backoff, because an
952
952
  OpenAI-compatible custom `baseUrl` may return anything — and a genuine, retry-exhausted rate
953
- limit still propagates, because masking that would hide a real outage. Every degrade code lives on
953
+ limit becomes typed availability after its retry budget, retaining the safe degraded reason. Every degrade code lives on
954
954
  the USE-CASE, so the `fs_search` tool inherits it, not just the route.
955
955
  Deterministic/Ollama models carry no credential and are never gated. Rules are set on the
956
956
  admin Credentials tab (exact-id). The `embedding.*` keys are ordinary counted credentials —
@@ -973,9 +973,9 @@ There is intentionally no README inside the contribution folders; this section i
973
973
  - `start(state)` begins the background vector activity watch (`startQdrantWatch` wires `usecases/vectorActivityWatch.ts`, every `brainQdrantWatchIntervalMs` — 30 s — `BRAIN_INFRA_MODE=local` only). The vector backend wired at init is **not** hot-swapped (out of scope); the watch surfaces a degraded flag and logs transitions — `vector.qdrant.lost` and `vector.qdrant.recovered` (the latter notes that a restart re-enables Qdrant-backed search when the wired backend is in-memory). While Qdrant is reachable the same tick reads the server's optimizer tracker (one `GET /collections/<c>/optimizations?with=completed`, at most 50 finished runs) — Qdrant does not log its own background optimization — and writes `vector.optimizer.completed {optimizer, status, points, segments, durationMs}` once per finished run (the SERVER's start/finish times, so a run shorter than the tick is not lost), `vector.optimizer.long` once for a run still going after 10 minutes, and `vector.optimizer.overflow {returned}` when every returned run is new while earlier ones were seen (the tracker rolled past runs between two reads). The first read only seeds: runs finished before the process started are not re-logged at every boot. These lines go to the console (`docker logs`) AND `vector.jsonl`; the per-probe `vector.qdrant.health` line stays file-only. The lifecycle `health(state)` returns structured backend/connectivity checks to the generic runtime health aggregate; no vector-status package API is exposed. The interval is cleared in `stop()`.
974
974
  - `onBootstrapComplete(state, ctx: BootstrapCompleteContext)` is the single deferred-wiring entry point: it reads the published registries through `ctx.platform.connectorRegistries()` and starts the `SyncScheduler` from `ctx.platform.hostIdentity()`. The factory resolves those registries lazily through its init context. There are no imperative setters (`setHostIdentity`, `setConnectorRegistries`).
975
975
  - `init` delegates to `wiring.buildBrainState`, wiring the declared `ctx.config` reader and GLOBAL-only `ctx.credentials.resolveCredential` into the embedding provider before shared infrastructure is built. Platform facts are read lazily through `ctx.platform`; no config setter or source-running bootstrap push is exposed. Credential changes reach the bounded lifecycle hook, and runtime health reads structured vector checks.
976
- - **Data formats.** brain-core's three durable record kinds — `brain-core/embedding-lock` (v2), `brain-core/source-config` (v1) and `brain-core/pending-changes` (v1, the `_pending/changes.jsonl` journal) — are declared on the kernel's data-format ledger at module load (`src/store/dataFormats.ts`) and claimed at the top of `init`, before the first read: a kind a newer binary raised stops the init by name. Per record, the lock's `version` and a source config's or pending change's `formatVersion` (absent = 1) are checked before every rewrite — a newer record is never rewritten (the journal's appends stay open; its compaction refuses). Source configs are written durably, and one that fails validation is skipped AND logged (`source-config.invalid-skipped`), never silently gone.
976
+ - **Data formats.** brain-core's three durable record kinds — `brain-core/embedding-lock` (v3), `brain-core/source-config` (v2) and `brain-core/pending-changes` (v1, the `_pending/changes.jsonl` journal) — are declared on the kernel's data-format ledger at module load (`src/store/dataFormats.ts`) and claimed at the top of `init`, before the first read: a kind a newer binary raised stops the init by name. Per record, the lock's `version` and a source config's or pending change's `formatVersion` (absent = 1) are checked before every rewrite — the check reads the STORED record, never the incoming one, so a config built fresh over a newer file is refused too — and a newer record is never rewritten (the journal's appends stay open; its compaction refuses). Every source-config write stamps the declared version (`SOURCE_CONFIG_FORMAT.version`, 2); an identical re-upsert still writes nothing. `embedding-lock` v3 and `source-config` v2 are `upgrade: offline` kinds: the app refuses to start over older data naming `neuralis upgrade`, which raises them (`scripts/embeddingLockV3.ts`, exported as `@neuralis/brain-core/upgrade`). Source configs are written durably, and one that fails validation is skipped AND logged (`source-config.invalid-skipped`), never silently gone.
977
977
  - **Declared credentials + dead-sweep.** brain-core's manifest declares the four embedding credentials — `embedding.openai`, `embedding.gemini`, `embedding.qwen`, `embedding.voyage` — in `neuralis.credentials[]` (`category: 'vector'`), so they appear in the admin catalog for an owner to set; an operator endpoint's `embedding.endpoint.<id>` is a dynamic family, never declared. `qdrant.apiKey` is deliberately NOT declared: `getQdrantClient()` is synchronous and boot-critical, so it structurally cannot read the credential store; the value comes from `QDRANT_API_KEY` in the boot env (Qdrant is infrastructure topology, not a per-scope secret — see `src/config/README.md`; setup writes the line itself and the server enforces the key on every request, with a mismatch surfacing as the structured `vector.qdrant.unauthorized` log line + a degraded Vector tab, not a boot death). `src/config/env.ts` was swept of dead credential plumbing: the old `resolveCredential()` helper, `BRAIN_CREDENTIAL_MAP`, and the `s3.*` / `media.apiKey` env reads (declared nowhere, consumed nowhere) are all removed. Only `getNeuralisEnv().qdrant.*` (URL/apiKey/collection — boot-critical infra topology from `process.env`) and the registry-driven embedding resolvers remain.
978
- - **Declared config settings.** brain-core's manifest declares its **78** platform-config keys in `neuralis.configSettings[]` (`PackageConfigSetting[]`) — sync/scan/chunking/media/git-panel/watch-recovery/vector-maintenance/vector-write-retry/pending-orphan-probe/search caps and timeouts across three categories: `brain` (72 keys), `vector` (5) and `debug` (1). (Re-count the manifest array before quoting this; it has been stale before.) The originals: the `brain` category (`brainAutoSyncMode`, `brainMaxScanFiles`, `inlineVideoMaxMb`, `brainSyncConcurrency`, `brainOllamaEmbedConcurrency`, `brainDebug` — `brainEmbedConcurrency` was DELETED: every embedder implements `embedBatch`, so the only fan-out fallback was dead code), the `vector` category (`embeddingModelId`, `embeddingDimension`, `ollamaUrl`, `openaiBaseUrl`, and `embeddingEndpoints` — written only through the host's endpoint route — rendered by the admin Vector tab, not the flat list), the embedding keys in `brain` (`qwenEmbeddingBaseUrl`, `embeddingDailyUsdCap`, `vectorKeepPreviousGeneration`), the media lane's `brainSyncMaxMediaMb` and `brainSyncMaxPdfPages`, and the `debug` category (`brainVectorLogConsole` — the stdout tee described under Observability below). The host registers them into `PlatformConfigStore` at bootstrap via the agent-core collector (first-party trust-gated); values reach brain-core through the injected `BrainConfigProvider`; the shared `getBrainConfigNumber(key, env, default)` live reader (config/env.ts) backs `brainSyncConcurrency` (DeltaSyncRunner) and `brainOllamaEmbedConcurrency` (the OllamaEmbedder semaphore, embeddings/factory.ts — applies on restart/embedder rebuild). The agent-core `configDeclarationDrift` test pins brain-core's consumed ⊆ declared set bidirectionally.
978
+ - **Declared config settings.** brain-core's manifest declares its **84** platform-config keys in `neuralis.configSettings[]` (`PackageConfigSetting[]`) — sync/scan/chunking/media/binary-sniff/git-panel/watch-recovery/vector-maintenance/vector-write-retry/pending-orphan-probe/search caps and timeouts across three categories: `brain` (78 keys), `vector` (5) and `debug` (1). (Re-count the manifest array before quoting this; it has been stale before.) The originals: the `brain` category (`brainAutoSyncMode`, `brainMaxScanFiles`, `inlineVideoMaxMb`, `brainSyncConcurrency`, `brainOllamaEmbedConcurrency`, `brainDebug` — `brainEmbedConcurrency` was DELETED: every embedder implements `embedBatch`, so the only fan-out fallback was dead code), the `vector` category (`embeddingModelId`, `embeddingDimension`, `ollamaUrl`, `openaiBaseUrl`, and `embeddingEndpoints` — written only through the host's endpoint route — rendered by the admin Vector tab, not the flat list), the embedding keys in `brain` (`embeddingEnabled`, `embeddingRequestTimeoutMs`, `qwenEmbeddingBaseUrl`, `embeddingDailyUsdCap`, `vectorKeepPreviousGeneration`), the media lane's `brainSyncMaxMediaMb` and `brainSyncMaxPdfPages`, and the `debug` category (`brainVectorLogConsole` — the stdout tee described under Observability below). The host registers them into `PlatformConfigStore` at bootstrap via the agent-core collector (first-party trust-gated); values reach brain-core through the injected `BrainConfigProvider`; the shared `getBrainConfigNumber(key, env, default)` live reader (config/env.ts) backs `brainSyncConcurrency` (DeltaSyncRunner) and `brainOllamaEmbedConcurrency` (the OllamaEmbedder semaphore, embeddings/factory.ts — applies on restart/embedder rebuild). The agent-core `configDeclarationDrift` test pins brain-core's consumed ⊆ declared set bidirectionally.
979
979
  - The `CommunityInfraFactory` uses the kernel’s bounded `ProjectServiceFactory` internally. Routes and tools resolve services via `state.infraFactory.getServices(projectId)` — no global default-project state.
980
980
  - Source connectors are declared in package manifests via `neuralis.connectors[]`. Connector **kinds** register into the host's `SourceKindRegistry` at boot (the factory templates); Brain-core itself contributes the built-in `local` and `brain` kinds via `LocalDiskConnector` and `BrainInternalConnector`. Connector **instances** are bound PER PROJECT — `buildCommunityFilesystemInfra` constructs a fresh `ConnectorRegistry` inside each project's `FilesystemInfra` and `registerForSource`s that project's source slugs into it. Two projects with the same local slugs (`data`/`packages`/`brain`/`app`) therefore never collide; the previously-shared host registry was last-writer-wins and leaked one tenant's filesystem (and poisoned the shared vector index) into another. The host-shared registry handed in at bootstrap is retained only as the boot-readiness gate, no longer a source-binding store.
981
981
  - Brain-core declares **no hooks**. The former `hooks.json` / `host.ts` `onFileCreated` / `onFileModified` PostToolUse callbacks were deleted: they were intentional no-ops (the create/modify use cases update the vector index inline), and their registration produced a noisy `[hooks] Callback resolution failed` warning whenever `dist/src/host.js` drifted. The `./host` package export was removed with them.
@@ -1094,16 +1094,7 @@ URI the same way it ingests `machine_use screenshot`.
1094
1094
  rather than as a re-resolvable `ref{uri}` — re-reading the `.pdf` uri on
1095
1095
  replay would re-render non-deterministically (or, with rendering off,
1096
1096
  return text → an empty image → a provider 400).
1097
- - **Capability gate (default emit).** `readFilesUseCase` attaches a
1098
- vision block whenever the connector returns `bytes`. It degrades the
1099
- file to a marker only when the runtime EXPLICITLY declares the matching
1100
- capability disabled — `runtime.modelCapabilities.imageInput === false`
1101
- or `videoInput === false`. Absent / partial `modelCapabilities`
1102
- (programmatic callers, agent-core boundaries that don't snapshot the
1103
- model registry) still emit the block; provider adapters that cannot
1104
- accept it strip it at adapter time. The explicit-deny marker reads
1105
- `[image/png 248 KB — imageInput disabled for this turn]`. This matches
1106
- the `machine_use screenshot` semantics (unconditional emit).
1097
+ - **Unconditional producer.** `readFilesUseCase` emits media whenever the connector supplies bytes and a recognized media MIME. Both direct-URI and indexed reads use the existing binary-extension table when a byte-bearing connector omits MIME. Unknown valid UTF-8 remains text; invalid UTF-8 and opaque files keep their source URI and an honest marker. Upload/write paths preserve original bytes. The agent-core per-step delivery boundary alone decides model acceptance, image integrity and explicitly declared subtype limits; converters only translate shapes.
1107
1098
  - **Per-batch budget.** The use-case sums approximate decoded sizes of
1108
1099
  every attached block; once the running total crosses 12 MB, remaining
1109
1100
  blocks downgrade to a `[batch image budget exceeded — request fewer
@@ -1339,7 +1330,7 @@ the package-manager content stores — `node_modules`, `.pnpm-store`, `.yarn/cac
1339
1330
  root and nested — because an in-container `pnpm install` on a bind-mounted
1340
1331
  repository lands `.pnpm-store/` at the repo root (5 124 files on one mounted
1341
1332
  package, 2026-08-27), and a source's `sync.exclude` is ADDITIVE over this floor,
1342
- so every existing source inherits a new floor entry at its next sync. Pattern/media exclusions remove obsolete parent/chunk search data. Recovery-bearing parents — pending/deleted records, saved baselines and sole-copy `brain://` content — retain their complete payload with `recovery_only:true`, no embedding stamp and no dense or BM25 vectors. Retrieval, browsing and review remain policy-gated; search, enrichment and source-package discovery exclude retained recovery points. Re-inclusion produces a real new embedding before clearing the marker. Enabled/auto inactivity never purges an index, and an immutable READ floor alone never authorizes cleanup. Generation rebuilding classifies current source policy before provider work and again at its config-coordinated swap: unknown provenance or unavailable/invalid configuration pauses without swapping. The dollar estimate samples only eligible content and exposes protected/excluded/unresolved counts; it remains a range, not a hard spending bound. Full sync runs this cleanup at its existing end; targeted sync never purges.
1333
+ so every existing source inherits a new floor entry at its next sync. Pattern/media exclusions remove obsolete parent/chunk search data. Recovery-bearing parents — pending/deleted records, saved baselines and sole-copy `brain://` content — retain their complete payload with `recovery_only:true`, no embedding stamp and no dense or BM25 vectors. Retrieval, browsing and review remain policy-gated; search, enrichment and source-package discovery exclude retained recovery points. Verified source input permits ordinary payload indexing again; missing/stale embedding debt remains explicit until real dense completion. Enabled/auto inactivity never purges an index, and an immutable READ floor alone never authorizes cleanup. Generation rebuilding classifies current source policy before provider work and again at its config-coordinated swap: unknown provenance or unavailable/invalid configuration pauses without swapping. The dollar estimate samples only eligible content and exposes protected/excluded/unresolved counts; it remains a range, not a hard spending bound. Full sync runs this cleanup at its existing end; targeted sync never purges.
1343
1334
 
1344
1335
  **The READ FLOOR is the INDEX FLOOR.** Since 2026-08-05 the immutable `floor: true`
1345
1336
  rows govern a THIRD axis: `isUriSyncIndexable` evaluates them alongside the
@@ -1501,7 +1492,7 @@ ADD gate remain AUTHORITATIVE for both families; a declaration's
1501
1492
  `visibility.seeRequires` is defense-in-depth, unioned into the SEE filter, never
1502
1493
  the boundary.
1503
1494
 
1504
- The package declares the optional singleton `package-source-roots` service, delegating to its existing source-root enumerator with scope, enabled state and package-recognition flags intact. Project infrastructure uses the loader's bounded project factory; running jobs and event subscriptions pin the same infrastructure through idle eviction. Permanent purge fences new queue work and waits for existing writes before vector deletion, subscription disposal and cache invalidation. Shutdown `drain` stops background admission and awaits completion bookkeeping under the runtime bound.
1495
+ The package declares the optional singleton `package-source-roots` service, delegating to its existing source-root enumerator with scope, enabled state and package-recognition flags intact, plus a change subscription off the one source-config commit funnel. Project infrastructure uses the loader's bounded project factory; running jobs and event subscriptions pin the same infrastructure through idle eviction. Permanent purge fences new queue work and waits for existing writes before vector deletion, subscription disposal and cache invalidation. Shutdown `drain` stops background admission and awaits completion bookkeeping under the runtime bound.
1505
1496
 
1506
1497
  ## Gotchas
1507
1498
 
@@ -1509,7 +1500,7 @@ The package declares the optional singleton `package-source-roots` service, dele
1509
1500
  - `FilePresence` is per-node: `brain` (vector-only), `synced` (index + connector agree), `unindexed` (on-disk but not yet indexed), `stale_indexed` (in index but no longer on connector). `fs_list` emits a single-character glyph (`●` / `○` / `✗`) only when a child's presence differs from its parent.
1510
1501
  - `fs_read` and `fs_list` are **connector-first**: they take live data (and, for `fs_list`, the tree structure) from the connector even when the vector index is cold or unreachable, and reconcile presence by a targeted index lookup over exactly the listed URIs. Consequence: the live-merge `fs_list` tree shows `brain` / `synced` / `unindexed` only — `stale_indexed` appears in the `only_indexed:true` view, on the source registry's `indexed_count` vs `on_disk` drift, and on a per-source connector failure (the broken source's indexed slice is surfaced as `stale_indexed`). See `src/usecases/README.md` for the phase/budget model. `fs_search` in `semantic` / `filter` modes requires the index to be warm; `grep` mode bypasses the index entirely and runs ripgrep / git grep / grep over the live connector. The keyword surface (filter mode + the hybrid text prefetch) rides the `content` full-text payload index (word tokenizer, lowercase, max token 255 — the Lucene/ES convention; the whole content is indexed, only over-length junk tokens are skipped) — `ensurePayloadIndexes` RECONCILES a drifted text index (param mismatch or missing params → delete + recreate) instead of no-opping on "already exists", so a legacy case-sensitive index heals on the first vector op after deploy. Search failures surface as fixed client-safe error rows with `count.errors` (backend detail is server-logged only). `fs_search` now enforces the per-uri `read` policy on every result across all three modes (closing the prior gap where it only feature-gated) — so a `read:false` path is never search-visible, and `grep` is the uri-policy-gated path to literal search without `execute`/shell.
1511
1502
  - Source slugs are URI schemes. Scoped clones use distinct schemes (`machine-alice://`, `local-bob://`) — never reuse a slug across scopes.
1512
- - The embedding lock (`src/embeddings/lock.ts`, `~/.neuralis/app/config/embedding-lock.json`) records the ACTIVE model and dimension; changing `embeddingModelId`/`embeddingDimension` only sets the target, and the switch is the generation rebuild — never a silent re-point, never a reset. `QDRANT_COLLECTION_DIMENSION_MISMATCH` (`src/vector/QdrantVectorIndex.ts`) still fires when the live collection's vector size disagrees with the lock. **`pnpm reset:vector`** drops every collection AND the lock — and with them the `brain://` content and its version history, which live only in Qdrant; it is the last resort, not the model switch. Config resolution (`src/config/env.ts`, surfaced through the embedder factory) throws `EMBEDDING_CREDENTIAL_MISSING` / `UNSUPPORTED_EMBEDDING_DIMENSION` instead of silently falling back to a different provider, dimension or key.
1503
+ - The v3 embedding lock (`src/embeddings/lock.ts`, `~/.neuralis/app/config/embedding-lock.json`) records metadata serving or the certified ACTIVE model and dimension; changing `embeddingModelId`/`embeddingDimension` only sets the target, and the switch is the generation rebuild — never a silent re-point, never a reset. `QDRANT_COLLECTION_DIMENSION_MISMATCH` (`src/vector/QdrantVectorIndex.ts`) still fires when the live collection's vector size disagrees with the lock. The explicit `dense-clear` collection action removes only dense embeddings, preserving text/payload/history and leaving durable catchup debt. **`pnpm reset:vector`** requires proven offline quiescence and drops owned collections AND the lock only after all selected deletions succeed — and with them the `brain://` content and its version history, which live only in Qdrant; it is the last resort, not the model switch. Config resolution (`src/config/env.ts`, surfaced through the embedder factory) throws `EMBEDDING_CREDENTIAL_MISSING` / `UNSUPPORTED_EMBEDDING_DIMENSION` instead of silently falling back to a different provider, dimension or key.
1513
1504
  - Every write snapshots `version.baseline` on the node, enabling `fs_write {revert:true}` (1-level undo) without an external embedder. The chain is the recovery contract — the policy layer does not consult a separate `getMutationSafety` resolver.
1514
1505
  - A write's success response also surfaces `version: { diff: { added, removed }, changedAt? }` (real previous-vs-next line delta from `computeLineChangeStats`, not an input estimate). The `filesystem.change` card and the chat composer's Changes & Actions panel consume this directly so the `+N/-M` badge stays accurate even for `replace_text` where the find/replace fragments don't equal the actual line change.
1515
1506
  - `commitWrite` (`usecases/write/commit.ts`) orders the slow phases so a write pays the least it can, and **every step of that order is an invariant** — the full rule, with its degrade matrix, lives in `src/usecases/README.md` and is stated there alone.
@@ -72,12 +72,14 @@ export const DiffView = memo(
72
72
 
73
73
  return (
74
74
  <div className="font-mono text-[11px] leading-[18px] overflow-x-auto overflow-y-clip">
75
- {elements}
76
- {truncated && (
77
- <div className="text-[10px] text-white/40 bg-white/4 px-3 py-1 text-center">
78
- ··· {hunks.reduce((n, h) => n + h.lines.length, 0) - maxLines} more lines ···
79
- </div>
80
- )}
75
+ <div className="min-w-full w-max">
76
+ {elements}
77
+ {truncated && (
78
+ <div className="w-full text-[10px] text-white/40 bg-white/4 px-3 py-1 text-center">
79
+ ··· {hunks.reduce((n, h) => n + h.lines.length, 0) - maxLines} more lines ···
80
+ </div>
81
+ )}
82
+ </div>
81
83
  </div>
82
84
  );
83
85
  },
@@ -111,7 +113,7 @@ export const DiffView = memo(
111
113
 
112
114
  function SkippedLines({ label }: { label: string }) {
113
115
  return (
114
- <div className="text-[10px] text-white/35 bg-white/[0.03] px-3 py-0.5 text-center border-y border-white/6 select-none">
116
+ <div className="w-full text-[10px] text-white/35 bg-white/[0.03] px-3 py-0.5 text-center border-y border-white/6 select-none">
115
117
  {label}
116
118
  </div>
117
119
  );
@@ -17,7 +17,7 @@ Filesystem agent-card: an IDE-style diff view rendered for `fs_write` tool calls
17
17
  - `estimateFilesystemChangeCardHeight` (exported from `FilesystemCardShell.tsx`) — the card's OWN row-height model, handed to agent-core's card registry at install time. agent-core's timeline asks the registry for it before a row has been measured; a card with no model falls back to a generic constant. It is declared next to the chrome it describes because it drifted badly when it lived in the timeline: the host-side table once claimed 240 against a real ~117 — a live-measured `first-measure` delta of **−181px**, the worst of any builtin. The **COMPACT branch is variant-aware** (H1, residual-slide audit 2026-08-07): the body is content-height clipped at `maxHeight: 90`, NOT a constant, so the model is `27 + min(90, bodyContent)` where `bodyContent` mirrors the render dispatch — `18 × diffLines` for a CREATE's input-derived rows, `32` for the one-line fallbacks (pending "Running…", "Done"/"Operation failed", non-content ops), and the **90 CEILING for a completed content MODIFY deliberately** (H2 pre-reserve: the real LCS diff arrives from an async fetch and adds context rows the input-derived count cannot see — ceiling bias accepted so the arrival does not resize the row in view). The previous constant-117 model over-reserved every short-bodied variant by +18…+72. The **EXPANDED branch** models the diff body from `computeLineCounts` (the same helper the header badge uses) with a deliberate upward margin, and is not optional: returning the compact height for a row the disclosure store says is expanded measured **−1353px** live (114 estimated against 1479 rendered), and an under-reserve does not leave a gap — it paints the next row on top of this one. The memo is keyed by the tool object **with the status it computed at** — the tool mutates by reference across pending→settle, so a reference-only memo served the pending height after settle (the H3 class). Keep both branches in step with `renderHeader` / `compactHeight` / `DiffLineRow`'s `h-[18px]`, and keep the export a module-level reference (the registry compares estimator identity). Contract: agent-core `app/chat/cards/README.md` → "Card height model".
18
18
  - `DiffView` — green/red line renderer with single-column line numbers, truncation and skipped-context markers.
19
19
  - `NonContentOpView` — summary renderer for `set_fields`, `move`, `delete`, `revert`.
20
- - `diffLine.tsx` — the **shared single-column diff-line primitive**: `DiffLineRow({ lineNo?, type, content })` + the `DIFF_LINE_BG` palette (`add` emerald / `remove` red / `context` colourless). ONE owning file for the green/red/colourless look + the line-number gutter. Its only consumer today is this card's `DiffView` — the Files-widget review diff moved to `@codemirror/merge` (`ReviewDiffView`) and the old hand-rolled `app/filesystem/ui/DiffView.tsx` was deleted. Keep it a standalone primitive: it is still the shared shape any future single-column diff renderer should reuse. No `+`/`-` prefix char — the row background conveys add/remove. **Gutter is git / VS Code unified-diff style:** every row numbered — REMOVE rows show their OLD (pre-edit) file line number, ADD + CONTEXT show the NEW one (so the column intentionally jumps back at a block replacement: old positions then new).
20
+ - `diffLine.tsx` — the **shared single-column diff-line primitive**: `DiffLineRow({ lineNo?, type, content })` + the `DIFF_LINE_BG` palette (`add` emerald / `remove` red / `context` colourless). ONE owning file for the green/red/colourless look + the line-number gutter. Its only consumer today is this card's `DiffView` — the Files-widget review diff moved to `@codemirror/merge` (`ReviewDiffView`) and the old hand-rolled `app/filesystem/ui/DiffView.tsx` was deleted. Keep it a standalone primitive: it is still the shared shape any future single-column diff renderer should reuse. No `+`/`-` prefix char — the row background conveys add/remove. `DiffView` owns one shared content-width wrapper; every row fills it so shorter coloured lines also cover the horizontal scroll extent. **Gutter is git / VS Code unified-diff style:** every row numbered — REMOVE rows show their OLD (pre-edit) file line number, ADD + CONTEXT show the NEW one (so the column intentionally jumps back at a block replacement: old positions then new).
21
21
  - Diff utilities (`diffUtils.ts`): input-derived hunk builders (`computeCreateDiff`, `computeReplaceTextDiff`, `computeMultiEditDiff`, `computeUpdateDiff`, `computeTextInsertDiff`, `computeLineCounts`) AND the real baseline→current LCS (`computeLcsDiff` + `countHunks`) shared with Changes. Callers must bound the O(n·m) matrix before invoking `computeLcsDiff`; pending previews use `pendingDiffWithinBudget`, because a byte cap alone does not bound newline-dense text. Plus `detectLanguage`, `isContentAction`, `extractContentString`, and `parseToolInput` (the streaming-tolerant `}`-tail-checked input parse — the fs-specific name for agent-core's shared `parseSettledObject`; ONE owner within brain-core for the card AND the height model) — which now also runs `normalizeCardInput`, the ONE place two write vocabularies are reconciled. `fs_write` carries its intent in KEYS (`edit`, `insert`, `lines`, `overwrite`, `create`, `fields`, `move`, `copy`, `mkdir`, `revert`, `delete`); the old pair carried it in `action` plus a flat field per shape. A verb-key input is PROJECTED onto the shape the views already read (`edit[].old_string/new_string` → the `replace_text` `edits[]` pair, so the red/green hunk appears before the tool result lands and a pending approval renders the PROPOSITION), and a row written by `fs_create`/`fs_modify` passes through untouched — which is what keeps the stored history rendering. A composed call previews the most informative verb (`edit` before `lines`, `insert`, `overwrite`, `create`). Pinned by `test/ui/verbKeyCardInput.test.ts`. Types: `DiffHunk`, `DiffLine`, `DiffLineType`.
22
22
  - Composer-aggregation ownership (`diffUtils.ts`, the composer-contract inversion): `extractResultVersionDiff` (the ONE `structuredContent.version.diff` reader — the card's `+N/−M` header badge AND the composer extractor share it; moved here from both its former private copy in `FilesystemChangeCard` and agent-core's deleted `extractResultDiff`), `extractFilesystemComposerChanges` (paths via agent-core's shared `extractPathsFromToolCall` + the former `deriveOpenPath` input-uri fallback for `ok:false` results; diff on the first entry), and `FILESYSTEM_COMPOSER_CONTRACT` (`{ tab: 'changes', changes }`, module-level stable) — registered in `installFilesystemHostComponents.tsx` so agent-core's Changes tab + post-stream summary learn about fs changes from the card REGISTRY, never from literals. Pinned by `test/ui/composerContract.test.ts` (parity: success uri+diff, diff-less results, `ok:false` input fallback, files[] multi-path, empty, and the reducer's string-input forms — a COMPLETE JSON string parses via the settled-object gate, a PARTIAL in-flight string yields nothing and is covered by agent-core's aggregate `openPath || label` fallback row).
23
23
 
@@ -34,11 +34,9 @@ export const DiffLineRow = memo(function DiffLineRow({
34
34
  type: DiffLineType;
35
35
  content: string;
36
36
  }) {
37
- // `min-w-full w-max`: the row is at least the container width (full-bleed
38
- // green/red background) and grows with long lines so the parent's
39
- // `overflow-x-auto` scrolls horizontally instead of clipping.
37
+ // Every row fills the shared content width, including space beyond shorter lines.
40
38
  return (
41
- <div className={`flex h-[18px] min-w-full w-max ${DIFF_LINE_BG[type]}`}>
39
+ <div className={`flex h-[18px] w-full ${DIFF_LINE_BG[type]}`}>
42
40
  <span className="shrink-0 w-10 text-right pr-2 select-none text-white/25 tabular-nums">
43
41
  {lineNo ?? ''}
44
42
  </span>
@@ -34,7 +34,7 @@ Files widget package — the React surface mounted by the host as the brain-core
34
34
 
35
35
  ## Pending Changes tab
36
36
 
37
- `ui/PendingChangesPanel.tsx` is the **Pending Changes** view in the Files widget — a journal of revertible filesystem changes that an agent has made (the durable backend is `src/store/PendingChangeStore.ts`; the route is `pending`, see `src/routes/README.md`). It offers two genuinely distinct views: a **compact list** (filename + full folder path, no content fetch) and **diff cards** (the default — opened inline, each card loads its hunks lazily from ONE fresh `preview` (baseline + current state, the record's own fingerprint) through `hooks/useVisibleDiffCards.ts`; on a source-revision change or a virtualizer remount the card keeps its last preview on screen while the fresh one loads, and the index badge shows only for the current key's answer; `+N/−M` counts are computed from the loaded hunks when the record carries none). Records are ordered **oldest-first** so the review walks first→last. A focused-card cursor (◄/► header buttons, plus `↑`/`↓`/`←`/`→`, `a`=approve, `r`=revert, `x`=dismiss when the panel is focused) an **Approve N** (two-step confirm; only records that need no acknowledgement) button and an **M need confirmation** button make a full sweep fast — the latter opens `PendingReviewDialog`, grouped by reason, 100 records per batch; a finished batch disables its group buttons and offers **Next batch (K left)**; approving advances to the next change. Each live (non-orphaned) row also carries a neutral-tone **Dismiss** (x) button — "accept-and-stop-reviewing": the change is kept exactly as it is (a present file stays on disk + indexed, an agent's delete stays deleted) and only the review entry resolves, with no connector write (the `pending` route's `dismiss` action; see `src/routes/README.md`). Clicking a filename opens the file in the tree (never replacing the panel). A filter bar narrows by source / agent / conversation, and the conversation chip is **clearable** (the `×` resets `pendingPanelFilter`). The panel is registered as `WorkspacePanelId += 'pending'` in the store, a Toolbar button, and a `FileEditor` tab + render branch — `FileEditor`'s `showFilePane` **excludes** the pending tab so the file header / amber pending banner / file-content layer never overlay the panel (they share one flex container). The tab is gated by `drive.read` (visible to every drive user), while the *content* is filtered per record server-side (owner isolation + uri-policy on the `pending` route). The chat composer's "Open in Files" button (agent-core Changes&Actions) opens this tab scoped to a conversation; this is navigation only and is distinct from the chat approve/deny `PendingInteraction` card.
37
+ `ui/PendingChangesPanel.tsx` is the **Pending Changes** view in the Files widget — a journal of revertible filesystem changes that an agent has made (the durable backend is `src/store/PendingChangeStore.ts`; the route is `pending`, see `src/routes/README.md`). It offers two genuinely distinct views: a **compact list** (filename + full folder path, no content fetch) and **diff cards** (the default — opened inline, each card loads its hunks lazily from ONE fresh `preview` (baseline + current state, the record's own fingerprint) through `hooks/useVisibleDiffCards.ts`; on a source-revision change or a virtualizer remount the card keeps its last preview on screen while the fresh one loads, and the index badge shows only for the current key's answer; `+N/−M` counts are computed from the loaded hunks when the record carries none). Records are ordered **oldest-first** so the review walks first→last. A focused-card cursor (◄/► header buttons, plus `↑`/`↓`/`←`/`→`, `a`=approve, `r`=revert, `x`=dismiss when the panel is focused) an **Approve N** (two-step confirm; only records that need no acknowledgement) button and an **M need confirmation** button make a full sweep fast — the latter opens `PendingReviewDialog`, grouped by reason, 100 records per batch; a finished batch disables its group buttons and offers **Next batch (K left)**; approving advances to the next change. Each live (non-orphaned) row also carries a neutral-tone **Dismiss** (x) button — "accept-and-stop-reviewing": the change is kept exactly as it is (a present file stays on disk + indexed, an agent's delete stays deleted) and only the review entry resolves, with no connector write (the `pending` route's `dismiss` action; see `src/routes/README.md`). Clicking a filename opens the file in the tree (never replacing the panel). A filter bar narrows by source / agent / conversation, and the conversation chip is **clearable** (the `×` resets `pendingPanelFilter`). The panel is registered as `WorkspacePanelId += 'pending'` in the store, a Toolbar button, and a `FileEditor` tab + render branch — `FileEditor`'s `showFilePane` **excludes** the pending tab so the file header / amber pending banner / file-content layer never overlay the panel (they share one flex container). The tab is gated by `drive.read` (visible to every drive user), while the *content* is filtered per record server-side (owner isolation + uri-policy on the `pending` route). The chat composer's "Open in Files" button (agent-core Changes&Actions) opens this tab scoped to a conversation; this is navigation only and is distinct from the chat approve/deny `PendingInteraction` card. A notification row opens the widget through the same one-shot `applyNav`: `open-pending` (the pending summary), `open-sources` (a source's sync/embedding outcome) and `open-vector-health` (an operator's collection action) — navigation only, every gate re-applied by the panel's own reads.
38
38
 
39
39
  ## Open-to-file tree hydration
40
40
 
@@ -1191,12 +1191,20 @@ function normalizeReembedEstimate(raw: unknown): ReembedEstimate | null {
1191
1191
  }
1192
1192
 
1193
1193
  /**
1194
- * Estimate a forced re-embed of the scope. The route answers BEFORE it touches
1195
- * the queue or the source's sync markers — an estimate has no side effect.
1194
+ * `reembed` = every file in scope again (`force`); `pending` = only the files
1195
+ * whose semantic embedding is missing or stale (`embeddingOnly`, the debt pass).
1196
1196
  */
1197
- export async function estimateReembed(params: ReembedScope): Promise<ReembedEstimate | null> {
1197
+ export type ReembedKind = 'reembed' | 'pending';
1198
+ const reembedVerb = (kind: ReembedKind) => (kind === 'pending' ? { embeddingOnly: true } : { force: true });
1199
+
1200
+ /**
1201
+ * Estimate the scope. The route answers BEFORE it touches the queue or the
1202
+ * source's sync markers — an estimate has no side effect. For `pending` the
1203
+ * figures are an upper bound: the whole scope, of which only the owed part runs.
1204
+ */
1205
+ export async function estimateReembed(params: ReembedScope, kind: ReembedKind = 'reembed'): Promise<ReembedEstimate | null> {
1198
1206
  return normalizeReembedEstimate(
1199
- await (await api()).post<unknown>('sources/reindex', { ...params, force: true, estimate: true }),
1207
+ await (await api()).post<unknown>('sources/reindex', { ...params, ...reembedVerb(kind), estimate: true }),
1200
1208
  );
1201
1209
  }
1202
1210
 
@@ -1207,8 +1215,9 @@ export async function estimateReembed(params: ReembedScope): Promise<ReembedEsti
1207
1215
  */
1208
1216
  export async function startReembed(
1209
1217
  params: ReembedScope,
1218
+ kind: ReembedKind = 'reembed',
1210
1219
  ): Promise<{ documents: number | null; truncated: boolean; job: SyncJobSnapshotRecord | null }> {
1211
- const raw = await (await api()).post<Record<string, unknown> | null>('sources/reindex', { ...params, force: true });
1220
+ const raw = await (await api()).post<Record<string, unknown> | null>('sources/reindex', { ...params, ...reembedVerb(kind) });
1212
1221
  const job = raw?.job && typeof raw.job === 'object' ? (raw.job as SyncJobSnapshotRecord) : null;
1213
1222
  return { documents: finiteOrNull(raw?.documents), truncated: raw?.truncated === true, job };
1214
1223
  }
@@ -373,6 +373,10 @@ export interface SourceConfigRecord {
373
373
  error?: string;
374
374
  /** Media files the run did not embed, by reason — absent when none. */
375
375
  mediaSkipped?: { textOnlyModel: number; off: number; tooLarge: number; unavailable: number; invalid: number };
376
+ /** Why the run left semantic embedding owed (`disabled`, `quota`, …) — the data still landed. */
377
+ embeddingDegraded?: string[];
378
+ /** Binary files the run did not index as text — absent when none. */
379
+ binarySkipped?: number;
376
380
  };
377
381
  };
378
382
  last_updated?: string;
@@ -385,6 +389,8 @@ export interface SourceConfigRecord {
385
389
  orphan?: boolean;
386
390
  orphan_hint?: string | null;
387
391
  observed?: boolean;
392
+ /** The platform's LIVE semantic-embedding intent (`embeddingEnabled`), read per request — never a last pass's echo. */
393
+ embedding_enabled?: boolean;
388
394
  /** Server-evaluated source maintenance authority. */
389
395
  canMaintain?: boolean;
390
396
  owner?: { userId?: string; agentId?: string };
@@ -400,7 +406,7 @@ export interface SourceConfigRecord {
400
406
  /** Why `live === false` — fixed enum, mapped to a display string in the UI. */
401
407
  live_reason?: SourceLiveReason;
402
408
  /**
403
- * R2b — local-kind source ALSO acts as a WASM-package source (additive to the
409
+ * R2b — local-kind source ALSO acts as a runtime package source (additive to the
404
410
  * default `_packages/` drop-zone). Owner/admin-effective to set; honored for
405
411
  * project AND user/agent scope (advertise isolation enforced downstream).
406
412
  */
@@ -456,7 +462,7 @@ export interface UpdateSourceConfigRequest {
456
462
  * the existing config, so disabling sync needs this explicit flag.
457
463
  */
458
464
  clearSync?: boolean;
459
- /** R2b — toggle WASM-package recognition (owner/admin-effective, local-only). */
465
+ /** R2b — toggle runtime package recognition (owner/admin-effective, local-only). */
460
466
  recognizesPackages?: boolean;
461
467
  }
462
468
 
@@ -478,7 +484,7 @@ export interface CreateSourceRequest {
478
484
  /** Per-source override of the platform `brainMaxScanFiles` scan cap. */
479
485
  maxScanFiles?: number;
480
486
  };
481
- /** R2b — create the (local-kind) source as a WASM-package source (owner/admin). */
487
+ /** R2b — create the (local-kind) source as a runtime package source (owner/admin). */
482
488
  recognizesPackages?: boolean;
483
489
  }
484
490
 
@@ -490,8 +496,8 @@ export interface CreateSourceRequest {
490
496
  export interface SyncJobSnapshotRecord {
491
497
  jobId: string;
492
498
  source: string;
493
- mode: 'full' | 'targeted' | 'purge';
494
- status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
499
+ mode: 'full' | 'targeted' | 'purge' | 'embedding';
500
+ status: 'pending' | 'running' | 'completed' | 'paused' | 'failed' | 'cancelled';
495
501
  userId: string;
496
502
  projectId: string;
497
503
  agentId: string;
@@ -508,6 +514,8 @@ export interface SyncJobSnapshotRecord {
508
514
  filesSkipped: number;
509
515
  durationMs: number;
510
516
  error?: string;
517
+ /** An embedding-only pass: what it embedded, and why it paused when it did. */
518
+ embedding?: { audited: number; embedded: number; settled: boolean; pausedReason?: string };
511
519
  };
512
520
  error?: string;
513
521
  }
@@ -626,9 +634,12 @@ export interface FileSystemState {
626
634
  chunk_end?: number;
627
635
  match_type?: 'semantic' | 'keyword' | 'hybrid';
628
636
  score?: number;
637
+ embedding_status?: string;
629
638
  }>;
630
639
  searchLoading: boolean;
631
640
  searchError?: string;
641
+ /** What the last search ran as when that differs from what was asked (`SearchEffect`). */
642
+ searchEffect: { requestedMode?: string; effectiveMode?: string; embeddingDegraded?: string; warning?: string };
632
643
  /**
633
644
  * `true` = enumerated by a successful listing and current · `false` = enumerated
634
645
  * once, then taken off screen by a collapse (no reconcile lists a hidden folder),
@@ -740,10 +751,18 @@ export type FSSearchResponse = {
740
751
  chunk_end?: number;
741
752
  match_type?: 'semantic' | 'keyword' | 'hybrid';
742
753
  version?: FileVersionInfo;
754
+ /** The point's dense proof: `current`, or why it is not (`missing` / `stale` / `unknown` / …). */
755
+ embedding_status?: string;
743
756
  error?: string;
744
757
  }>;
745
758
  count: { found: number; errors: number };
746
759
  usage?: { vectorQueries?: number; embeddingCalls?: number };
760
+ /** The mode asked for, and the one that ran — a semantic request runs as `filter` when embedding is unavailable. */
761
+ requested_mode?: 'semantic' | 'filter' | 'grep';
762
+ effective_mode?: 'semantic' | 'filter' | 'grep';
763
+ /** Why semantic search fell back (`disabled`, `quota`, `credentials`, …). */
764
+ embedding_degraded?: string;
765
+ warning?: string;
747
766
  };
748
767
 
749
768
  export type FSUploadResponse = {
@@ -756,7 +756,7 @@ function evictColdSubtrees(
756
756
  }
757
757
 
758
758
  type FilesystemNavRef =
759
- | { kind: 'filesystem'; action: string; uri?: string; query?: string; filter?: PendingPanelFilter }
759
+ | { kind: 'filesystem'; action: string; uri?: string; query?: string; filter?: PendingPanelFilter; source?: string }
760
760
  | undefined;
761
761
 
762
762
 
@@ -897,6 +897,14 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
897
897
  */
898
898
  pendingSourceSuggestion: DiscoverableSourceSuggestion | null;
899
899
  setPendingSourceSuggestion: (suggestion: DiscoverableSourceSuggestion | null) => void;
900
+ /**
901
+ * Is the Sources panel's Vector Health section open? In the STORE so a
902
+ * notification's `open-vector-health` nav can open it while the panel is not
903
+ * mounted; context data (a `contextResetPatch` key), never persisted. The
904
+ * section still mounts its panel only while open (no CSS-only collapse).
905
+ */
906
+ vectorHealthOpen: boolean;
907
+ setVectorHealthOpen: (open: boolean) => void;
900
908
  setWorkspacePanelOpen: (panel: WorkspacePanelId, open: boolean) => void;
901
909
  setActiveWorkspaceTab: (tab: 'file' | 'git-diff' | 'git-commit' | WorkspacePanelId) => void;
902
910
  diffViewMode: DiffViewMode;
@@ -1110,6 +1118,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1110
1118
  workspacePanels: { upload: false, sources: false, packages: false, pending: false },
1111
1119
  activeWorkspaceTab: 'file',
1112
1120
  pendingSourceSuggestion: null,
1121
+ vectorHealthOpen: false,
1113
1122
  diffViewMode: 'auto',
1114
1123
  pendingPanelFilter: {},
1115
1124
  // Nothing is hydrated at boot. `brain://` is EXPANDED by default (below) and the
@@ -1180,6 +1189,7 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
1180
1189
  return { workspacePanels: nextPanels, activeWorkspaceTab };
1181
1190
  }),
1182
1191
  setPendingSourceSuggestion: (suggestion) => set(() => ({ pendingSourceSuggestion: suggestion })),
1192
+ setVectorHealthOpen: (open) => set(() => ({ vectorHealthOpen: open })),
1183
1193
  setActiveWorkspaceTab: (tab) => set(() => ({ activeWorkspaceTab: tab })),
1184
1194
  setDiffViewMode: (mode) => set(() => ({ diffViewMode: mode })),
1185
1195
  setPreviewEditorsEnabled: (enabled) => set(() => ({ previewEditorsEnabled: enabled })),
@@ -2217,6 +2227,15 @@ export const useFilesystemStore = create<FilesystemStore>()(persist((set, get) =
2217
2227
  get().setWorkspacePanelOpen('pending', true);
2218
2228
  return;
2219
2229
  }
2230
+ // A notification's target: the Sources panel (a source's sync or embedding
2231
+ // outcome), or its Vector Health section (an operator's collection action).
2232
+ // Navigation only — the panel's own listing re-applies every gate.
2233
+ if (action === 'open-sources' || action === 'open-vector-health') {
2234
+ set(() => ({ activeAgentId: agentId, activeProjectId: projectId,
2235
+ ...(action === 'open-vector-health' ? { vectorHealthOpen: true } : {}) }));
2236
+ get().setWorkspacePanelOpen('sources', true);
2237
+ return;
2238
+ }
2220
2239
  if (action === 'list' && uri) {
2221
2240
  set((state) => {
2222
2241
  const root = sourceRootUri(uri);
@@ -92,10 +92,12 @@ export function contextResetPatch(): Record<string, unknown> {
92
92
  // Sources panel must not survive an agent/project switch — the panel
93
93
  // would then prefill an Add-Source form for the PREVIOUS context.
94
94
  pendingSourceSuggestion: null,
95
+ vectorHealthOpen: false,
95
96
  searchQuery: '',
96
97
  searchResults: [],
97
98
  searchLoading: false,
98
99
  searchError: undefined,
100
+ searchEffect: {},
99
101
  // Git Control state resets with the context (gitViewMode — a UI
100
102
  // preference — intentionally persists, mirroring searchMode).
101
103
  gitStatus: null,
@@ -13,6 +13,16 @@ export type SearchSliceHit = {
13
13
  chunk_end?: number;
14
14
  match_type?: 'semantic' | 'keyword' | 'hybrid';
15
15
  score?: number;
16
+ /** `current`, or why the point's semantic embedding is not (`missing` / `stale` / …). */
17
+ embedding_status?: string;
18
+ };
19
+
20
+ /** What the last search ran as, when that differs from what was asked (or carries a warning). */
21
+ export type SearchEffect = {
22
+ requestedMode?: string;
23
+ effectiveMode?: string;
24
+ embeddingDegraded?: string;
25
+ warning?: string;
16
26
  };
17
27
 
18
28
  /** UI search modes → wire `search_mode`: keyword→filter, semantic→semantic, grep→grep. */
@@ -39,6 +49,8 @@ export interface SearchSliceState {
39
49
  searchResults: SearchSliceHit[];
40
50
  searchLoading: boolean;
41
51
  searchError?: string;
52
+ /** Empty when the search ran as asked, with no warning. */
53
+ searchEffect: SearchEffect;
42
54
  }
43
55
 
44
56
  export interface SearchSliceActions {
@@ -66,6 +78,7 @@ export const searchSliceInitial: SearchSliceState = {
66
78
  searchResults: [],
67
79
  searchLoading: false,
68
80
  searchError: undefined,
81
+ searchEffect: {},
69
82
  };
70
83
 
71
84
  /** Grep needs a live-connector folder scope; brain:// has no live text to scan. */
@@ -94,6 +107,7 @@ export function createSearchSlice(
94
107
  searchResults: [],
95
108
  searchLoading: false,
96
109
  searchError: undefined,
110
+ searchEffect: {},
97
111
  })),
98
112
 
99
113
  async runSearch({ agentId, projectId }) {
@@ -108,7 +122,7 @@ export function createSearchSlice(
108
122
  ? query.length >= 2 || glob.length >= 2
109
123
  : query.length >= 2;
110
124
  if (!agentId || !runnable) {
111
- set(() => ({ searchResults: [], searchLoading: false, searchError: undefined }));
125
+ set(() => ({ searchResults: [], searchLoading: false, searchError: undefined, searchEffect: {} }));
112
126
  return;
113
127
  }
114
128
 
@@ -127,6 +141,7 @@ export function createSearchSlice(
127
141
  searchResults: [],
128
142
  searchLoading: false,
129
143
  searchError: 'Grep needs a non-brain folder scope — select a folder in a disk-backed source.',
144
+ searchEffect: {},
130
145
  }));
131
146
  return;
132
147
  }
@@ -170,11 +185,18 @@ export function createSearchSlice(
170
185
  chunk_end: r.chunk_end,
171
186
  match_type: r.match_type,
172
187
  score: r.score,
188
+ ...(typeof r.embedding_status === 'string' ? { embedding_status: r.embedding_status } : {}),
173
189
  }));
174
190
  set(() => ({
175
191
  searchResults: hits,
176
192
  searchLoading: false,
177
193
  searchError: errorRow ? String((errorRow as any).error) : undefined,
194
+ searchEffect: {
195
+ ...(res.requested_mode ? { requestedMode: res.requested_mode } : {}),
196
+ ...(res.effective_mode ? { effectiveMode: res.effective_mode } : {}),
197
+ ...(res.embedding_degraded ? { embeddingDegraded: res.embedding_degraded } : {}),
198
+ ...(res.warning ? { warning: res.warning } : {}),
199
+ },
178
200
  }));
179
201
  } catch (error) {
180
202
  if (mySeq !== searchSeq) return;
@@ -182,6 +204,7 @@ export function createSearchSlice(
182
204
  searchResults: [],
183
205
  searchLoading: false,
184
206
  searchError: error instanceof Error ? error.message : 'Search failed',
207
+ searchEffect: {},
185
208
  }));
186
209
  }
187
210
  },