neotoma 0.24.0 → 0.26.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 (496) hide show
  1. package/README.md +80 -48
  2. package/dist/actions.d.ts +116 -27
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +611 -203
  5. package/dist/actions.js.map +1 -1
  6. package/dist/cli/agents_grants_import.d.ts.map +1 -1
  7. package/dist/cli/agents_grants_import.js +22 -2
  8. package/dist/cli/agents_grants_import.js.map +1 -1
  9. package/dist/cli/index.d.ts +29 -2
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/index.js +1011 -221
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/issues.d.ts +6 -0
  14. package/dist/cli/issues.d.ts.map +1 -1
  15. package/dist/cli/issues.js +47 -6
  16. package/dist/cli/issues.js.map +1 -1
  17. package/dist/cli/parse_schema_fields.d.ts +2 -1
  18. package/dist/cli/parse_schema_fields.d.ts.map +1 -1
  19. package/dist/cli/parse_schema_fields.js +4 -1
  20. package/dist/cli/parse_schema_fields.js.map +1 -1
  21. package/dist/cli/skills_mirror.d.ts +29 -6
  22. package/dist/cli/skills_mirror.d.ts.map +1 -1
  23. package/dist/cli/skills_mirror.js +70 -13
  24. package/dist/cli/skills_mirror.js.map +1 -1
  25. package/dist/docs/api/rest_api.md +169 -365
  26. package/dist/docs/architecture/architecture.md +250 -830
  27. package/dist/docs/architecture/idempotence_pattern.md +101 -0
  28. package/dist/docs/conventions/documentation_standards.md +184 -75
  29. package/dist/docs/developer/agentic_eval_fixture_format.md +2 -0
  30. package/dist/docs/developer/atomic_corrections.md +17 -0
  31. package/dist/docs/developer/cli_agent_instructions.md +33 -5
  32. package/dist/docs/developer/cli_reference.md +219 -170
  33. package/dist/docs/developer/fleet_onboarding.md +7 -6
  34. package/dist/docs/developer/mcp/instructions.md +34 -23
  35. package/dist/docs/developer/mcp_overview.md +4 -4
  36. package/dist/docs/integrations/matrix.md +4 -4
  37. package/dist/docs/operations/configuration.md +2 -1
  38. package/dist/docs/operations/running_the_server.md +1 -1
  39. package/dist/docs/reference/error_codes.md +23 -10
  40. package/dist/docs/skills/skill_strategy.md +10 -6
  41. package/dist/docs/specs/MCP_SPEC.md +26 -9
  42. package/dist/docs/subsystems/agent_attribution_integration.md +159 -0
  43. package/dist/docs/subsystems/agent_capabilities.md +57 -1
  44. package/dist/docs/subsystems/agentic_eval.md +13 -2
  45. package/dist/docs/subsystems/deletion.md +24 -9
  46. package/dist/docs/subsystems/errors.md +80 -45
  47. package/dist/docs/subsystems/ingestion/ingestion.md +1 -1
  48. package/dist/docs/subsystems/issues.md +45 -5
  49. package/dist/docs/subsystems/reducer.md +208 -52
  50. package/dist/docs/subsystems/schema_registry.md +39 -8
  51. package/dist/docs/subsystems/sources.md +25 -0
  52. package/dist/docs/subsystems/substrate_events.md +1 -1
  53. package/dist/docs/testing/automated_test_catalog.md +98 -20
  54. package/dist/docs/testing/testing_standard.md +33 -0
  55. package/dist/docs/use_cases/README.md +13 -13
  56. package/dist/docs/use_cases/agent_auth.md +1 -1
  57. package/dist/docs/use_cases/cases.md +1 -2
  58. package/dist/docs/use_cases/compliance.md +0 -1
  59. package/dist/docs/use_cases/contracts.md +1 -2
  60. package/dist/docs/use_cases/crypto_engineering.md +0 -1
  61. package/dist/docs/use_cases/customer_ops.md +1 -2
  62. package/dist/docs/use_cases/diligence.md +1 -2
  63. package/dist/docs/use_cases/government.md +1 -2
  64. package/dist/docs/use_cases/healthcare.md +1 -2
  65. package/dist/docs/use_cases/logistics.md +1 -2
  66. package/dist/docs/use_cases/portfolio.md +1 -2
  67. package/dist/docs/use_cases/procurement.md +1 -2
  68. package/dist/docs/use_cases/trading.md +1 -2
  69. package/dist/docs/vocabulary/canonical_terms.md +212 -180
  70. package/dist/embeddings.d.ts +4 -1
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/embeddings.js +7 -10
  73. package/dist/embeddings.js.map +1 -1
  74. package/dist/inspector/assets/{access_policies-B1HM9aJQ.js → access_policies-CJOlV60d.js} +1 -1
  75. package/dist/inspector/assets/{agent_badge-XmciduFJ.js → agent_badge-BnBqn8kt.js} +1 -1
  76. package/dist/inspector/assets/{agent_detail-DOEe2mhV.js → agent_detail-wP0ccjhX.js} +1 -1
  77. package/dist/inspector/assets/{agent_filter-Dio0XybG.js → agent_filter-Bp2xA131.js} +1 -1
  78. package/dist/inspector/assets/{agent_grant_detail-BfUFnclY.js → agent_grant_detail-HIGr5dq_.js} +1 -1
  79. package/dist/inspector/assets/agent_grant_form-BoNBvzEf.js +1 -0
  80. package/dist/inspector/assets/{agent_grants-DZaHIPk8.js → agent_grants-CTKkadE5.js} +1 -1
  81. package/dist/inspector/assets/{agents-B-f-NRE2.js → agents-CPwJLNJN.js} +1 -1
  82. package/dist/inspector/assets/{api_not_configured_state-Ou-G3uKz.js → api_not_configured_state-ivqIcs2c.js} +1 -1
  83. package/dist/inspector/assets/{attribution_card-CIkjiijF.js → attribution_card-5ZAqRz6K.js} +1 -1
  84. package/dist/inspector/assets/{attribution_summary-0ckzYad9.js → attribution_summary-D_MHFv7e.js} +1 -1
  85. package/dist/inspector/assets/{bundles-Burq1ci7.js → bundles-QfMciBj9.js} +1 -1
  86. package/dist/inspector/assets/{card-CTv_O7eJ.js → card-1rkzdi9W.js} +1 -1
  87. package/dist/inspector/assets/{checkbox--jk4IAjc.js → checkbox-CO5dXSZf.js} +1 -1
  88. package/dist/inspector/assets/{compliance-B3vrfBCU.js → compliance-OTGEtY7Y.js} +1 -1
  89. package/dist/inspector/assets/{confirm-dialog-DgDSsLkR.js → confirm-dialog-D0cZ5vck.js} +1 -1
  90. package/dist/inspector/assets/{conversation_common-BAdG50H9.js → conversation_common-cSILaHXq.js} +1 -1
  91. package/dist/inspector/assets/{conversation_detail-DIUc4HKE.js → conversation_detail-DT-lGapN.js} +1 -1
  92. package/dist/inspector/assets/{copy_id_button-D1VkPBFT.js → copy_id_button-Dz0JDn4l.js} +1 -1
  93. package/dist/inspector/assets/{copyable_code_block-ChPCJ02V.js → copyable_code_block-B2f99g8Q.js} +1 -1
  94. package/dist/inspector/assets/{corrections-ChJ2p7_i.js → corrections-KvxGvvLV.js} +1 -1
  95. package/dist/inspector/assets/{dashboard-DIYNcYg6.js → dashboard-BudvW21X.js} +1 -1
  96. package/dist/inspector/assets/{data-table-C4VXfuGY.js → data-table-d9EMxowf.js} +1 -1
  97. package/dist/inspector/assets/{design-DXOwL75T.js → design-DyiQbEeP.js} +1 -1
  98. package/dist/inspector/assets/{dialog-CXVzcRia.js → dialog-BmW1Iw8o.js} +1 -1
  99. package/dist/inspector/assets/{docs-B26tYp-6.js → docs-CT3IYX0E.js} +1 -1
  100. package/dist/inspector/assets/{docs_page_layout-CrM6Un_f.js → docs_page_layout-CosNjUZa.js} +1 -1
  101. package/dist/inspector/assets/{embed_graph-C2Jc2iwk.js → embed_graph-Dk1-zVnz.js} +1 -1
  102. package/dist/inspector/assets/{empty_state-d5guw2_7.js → empty_state-B8Ot2oGB.js} +1 -1
  103. package/dist/inspector/assets/{entities-BrnaqwF9.js → entities-Dt5Ts5UN.js} +1 -1
  104. package/dist/inspector/assets/{entity_correct-DnD1oYUl.js → entity_correct-D2PHEpGY.js} +1 -1
  105. package/dist/inspector/assets/{entity_history-PFlm7n3S.js → entity_history-DHgNythA.js} +1 -1
  106. package/dist/inspector/assets/{entity_history_sections-C5gKEqH8.js → entity_history_sections-Cnxblw-Z.js} +1 -1
  107. package/dist/inspector/assets/{entity_link-BlAO6pqg.js → entity_link-DWf1VD1n.js} +1 -1
  108. package/dist/inspector/assets/{entity_relationships_by_type-DzLwN95P.js → entity_relationships_by_type-B0-TLC5o.js} +1 -1
  109. package/dist/inspector/assets/{entity_segment_page-CGm6UKGn.js → entity_segment_page-CzZ0VhKm.js} +1 -1
  110. package/dist/inspector/assets/{entity_table_column_toggle-CotVPBog.js → entity_table_column_toggle-C73aErGE.js} +1 -1
  111. package/dist/inspector/assets/{entity_timeline-DYqizEmo.js → entity_timeline-SAq5qvB5.js} +1 -1
  112. package/dist/inspector/assets/{entity_types-CntHwuK5.js → entity_types-DEcDvyO0.js} +1 -1
  113. package/dist/inspector/assets/{faq-_NXFNTgM.js → faq-DazWIFdv.js} +1 -1
  114. package/dist/inspector/assets/{graph_auto_fit-BeH9MOss.js → graph_auto_fit-CSzD7yNh.js} +1 -1
  115. package/dist/inspector/assets/{graph_explorer-CxWZohQv.js → graph_explorer-BEXXiMQ1.js} +1 -1
  116. package/dist/inspector/assets/{home-DudMw6_Z.js → home-3AZtmjKp.js} +1 -1
  117. package/dist/inspector/assets/{index-DDgXel2r.js → index-Cii0RALL.js} +1 -1
  118. package/dist/inspector/assets/{index-VT5Dxs36.js → index-Dt_q_A1j.js} +1 -1
  119. package/dist/inspector/assets/{index-BMURChxZ.js → index-fc4sMprn.js} +2 -2
  120. package/dist/inspector/assets/{index-WhUlQzmL.css → index-yoY5L3eB.css} +1 -1
  121. package/dist/inspector/assets/interpretations-CjR2csNL.js +1 -0
  122. package/dist/inspector/assets/{interpretations-xJnQ-uYX.js → interpretations-DtzoiGuK.js} +1 -1
  123. package/dist/inspector/assets/{issue_author_attribution-DOn6PLKU.js → issue_author_attribution-DtmFaluD.js} +1 -1
  124. package/dist/inspector/assets/{issue_detail-DztHpggK.js → issue_detail-ChHiz9I_.js} +1 -1
  125. package/dist/inspector/assets/{issues-BkgvGReD.js → issues-BoxR2nB2.js} +1 -1
  126. package/dist/inspector/assets/{json_viewer-xKCfoCA1.js → json_viewer-KAFp_j-8.js} +1 -1
  127. package/dist/inspector/assets/{label-DDkYZSlU.js → label-Dn0ARjYl.js} +1 -1
  128. package/dist/inspector/assets/{live_relative_time-D-U5_U5-.js → live_relative_time-gOIFOOLR.js} +1 -1
  129. package/dist/inspector/assets/{mobile_filter_popover-DQNcOXkp.js → mobile_filter_popover-2ezU-mlZ.js} +1 -1
  130. package/dist/inspector/assets/{not_found-D6QKUWRM.js → not_found-DwX7ikQC.js} +1 -1
  131. package/dist/inspector/assets/{oauth_callback-BVxx0KXw.js → oauth_callback-BDGLOyTN.js} +1 -1
  132. package/dist/inspector/assets/{oauth_signin-DI4E46_1.js → oauth_signin-CGcCtq-V.js} +1 -1
  133. package/dist/inspector/assets/{observations-C1wQYMZV.js → observations-DvcUGpHm.js} +1 -1
  134. package/dist/inspector/assets/{page_shell-EP5xmaGY.js → page_shell-DiwchdUC.js} +1 -1
  135. package/dist/inspector/assets/{pagination-B_G7Mbyi.js → pagination-BGIUHQkm.js} +1 -1
  136. package/dist/inspector/assets/{peer_detail-BC-DSDif.js → peer_detail-DeD1sTeJ.js} +1 -1
  137. package/dist/inspector/assets/{peers-fVyfV57y.js → peers-CXe47m55.js} +1 -1
  138. package/dist/inspector/assets/{pin_primitive_button-_TIZX9AI.js → pin_primitive_button-C37Ha0Bt.js} +1 -1
  139. package/dist/inspector/assets/{recent_activity-CFv4LsTR.js → recent_activity-CzQjt4ym.js} +1 -1
  140. package/dist/inspector/assets/{recent_conversations-DU1XrX7M.js → recent_conversations-BFYZTxUJ.js} +1 -1
  141. package/dist/inspector/assets/{recent_conversations-Cac7CkDg.js → recent_conversations-BnUl4h4t.js} +1 -1
  142. package/dist/inspector/assets/{recent_records_feed-BFJzHH4J.js → recent_records_feed-y1n0RuMa.js} +1 -1
  143. package/dist/inspector/assets/record_activity-kXm-jtQ6.js +1 -0
  144. package/dist/inspector/assets/{relationship_detail-Cc9dVYco.js → relationship_detail-ic83ZCwG.js} +1 -1
  145. package/dist/inspector/assets/{relationship_panel_groups-Bknw8oKA.js → relationship_panel_groups-CI1fZIU_.js} +1 -1
  146. package/dist/inspector/assets/{relationships-DoCpO8F8.js → relationships-C1JrEl85.js} +1 -1
  147. package/dist/inspector/assets/{relationships-CJ7GneMs.js → relationships-C7iSdgXG.js} +1 -1
  148. package/dist/inspector/assets/{sandbox-BrYkvSj5.js → sandbox-Dst-WJhB.js} +1 -1
  149. package/dist/inspector/assets/{schema_detail-CdX2n82a.js → schema_detail-C7_JSWAw.js} +1 -1
  150. package/dist/inspector/assets/{schemas-CR6vDZ-d.js → schemas-BgrIpRVJ.js} +1 -1
  151. package/dist/inspector/assets/{search-uJmTOqpx.js → search-BUmT32dy.js} +1 -1
  152. package/dist/inspector/assets/{segmented_control-Dgdqohml.js → segmented_control-t1pBImFE.js} +1 -1
  153. package/dist/inspector/assets/{select-BCmhrYiM.js → select-DU7Jwzuy.js} +1 -1
  154. package/dist/inspector/assets/{settings-DRbR3maP.js → settings-BAPHY6RY.js} +1 -1
  155. package/dist/inspector/assets/{source_content-BuBSyT72.js → source_content-CIhKrGsl.js} +1 -1
  156. package/dist/inspector/assets/{source_content_open_button-DAYwX9Dh.js → source_content_open_button-Erw2ErgG.js} +1 -1
  157. package/dist/inspector/assets/{source_detail-DhEV5aJv.js → source_detail-C0XOo0Uu.js} +1 -1
  158. package/dist/inspector/assets/{source_link-CE1kUYaL.js → source_link-CkcRMEO3.js} +1 -1
  159. package/dist/inspector/assets/{sources-LZmXkGnn.js → sources-C0JBEINb.js} +1 -1
  160. package/dist/inspector/assets/{subscriptions-b_DO87jI.js → subscriptions-DxU4kHGt.js} +1 -1
  161. package/dist/inspector/assets/{switch-B6x__4FK.js → switch-BhKadp29.js} +1 -1
  162. package/dist/inspector/assets/{tabs-Bi6BUJqc.js → tabs-d81XObYb.js} +1 -1
  163. package/dist/inspector/assets/{textarea-3QlsHqkf.js → textarea-DTiKLH24.js} +1 -1
  164. package/dist/inspector/assets/{timeline-BoE9ZRrA.js → timeline-98QXcGln.js} +1 -1
  165. package/dist/inspector/assets/{timeline-a2L8UjV_.js → timeline-BEyKQpER.js} +1 -1
  166. package/dist/inspector/assets/{timeline_event_detail-D8eTp4i6.js → timeline_event_detail-DzGzqtrx.js} +1 -1
  167. package/dist/inspector/assets/{turn_detail-B2uAxNHK.js → turn_detail-CDP_FEPT.js} +1 -1
  168. package/dist/inspector/assets/{turns-BpYk8UFz.js → turns-DLmr1Iue.js} +1 -1
  169. package/dist/inspector/assets/{use_agents-Bt3UqR6w.js → use_agents-D3rvlr5v.js} +1 -1
  170. package/dist/inspector/assets/{use_entities-wuMfztjo.js → use_entities-CVikeUOv.js} +1 -1
  171. package/dist/inspector/assets/use_interpretations-CCoQ6ihs.js +1 -0
  172. package/dist/inspector/assets/{use_mutations-BhRvMBFs.js → use_mutations-BhwSLXcJ.js} +1 -1
  173. package/dist/inspector/assets/{use_peers-CD_9h7zQ.js → use_peers-QTm1Z_BV.js} +1 -1
  174. package/dist/inspector/assets/{use_relationships-CjsGAwR8.js → use_relationships-DtymxAI9.js} +1 -1
  175. package/dist/inspector/assets/{use_stats-IyFfTD4L.js → use_stats-BW1Z-dI2.js} +1 -1
  176. package/dist/inspector/assets/{use_timeline-BueAlCUW.js → use_timeline-DND87Sof.js} +1 -1
  177. package/dist/inspector/assets/{use_turns-CZtZ839A.js → use_turns-Dfb6Q8Ru.js} +1 -1
  178. package/dist/inspector/assets/{world_time_event_timeline-D8bL35LA.js → world_time_event_timeline-DxzejNrJ.js} +1 -1
  179. package/dist/inspector/index.html +2 -2
  180. package/dist/maintenance/entity_lifecycle_inventory.d.ts +31 -0
  181. package/dist/maintenance/entity_lifecycle_inventory.d.ts.map +1 -0
  182. package/dist/maintenance/entity_lifecycle_inventory.js +107 -0
  183. package/dist/maintenance/entity_lifecycle_inventory.js.map +1 -0
  184. package/dist/maintenance/entity_lifecycle_manifest.d.ts +84 -0
  185. package/dist/maintenance/entity_lifecycle_manifest.d.ts.map +1 -0
  186. package/dist/maintenance/entity_lifecycle_manifest.js +225 -0
  187. package/dist/maintenance/entity_lifecycle_manifest.js.map +1 -0
  188. package/dist/maintenance/entity_lifecycle_migration.d.ts +11 -0
  189. package/dist/maintenance/entity_lifecycle_migration.d.ts.map +1 -0
  190. package/dist/maintenance/entity_lifecycle_migration.js +576 -0
  191. package/dist/maintenance/entity_lifecycle_migration.js.map +1 -0
  192. package/dist/maintenance/entity_lifecycle_records.d.ts +12 -0
  193. package/dist/maintenance/entity_lifecycle_records.d.ts.map +1 -0
  194. package/dist/maintenance/entity_lifecycle_records.js +95 -0
  195. package/dist/maintenance/entity_lifecycle_records.js.map +1 -0
  196. package/dist/mcp_ws_bridge.js +26 -3
  197. package/dist/mcp_ws_bridge.js.map +1 -1
  198. package/dist/reducers/observation_reducer.d.ts +67 -3
  199. package/dist/reducers/observation_reducer.d.ts.map +1 -1
  200. package/dist/reducers/observation_reducer.js +202 -70
  201. package/dist/reducers/observation_reducer.js.map +1 -1
  202. package/dist/repositories/db/connection.d.ts +2 -0
  203. package/dist/repositories/db/connection.d.ts.map +1 -1
  204. package/dist/repositories/db/connection.js +27 -0
  205. package/dist/repositories/db/connection.js.map +1 -1
  206. package/dist/repositories/db/store_condition_schema.d.ts +3 -0
  207. package/dist/repositories/db/store_condition_schema.d.ts.map +1 -0
  208. package/dist/repositories/db/store_condition_schema.js +14 -0
  209. package/dist/repositories/db/store_condition_schema.js.map +1 -0
  210. package/dist/repositories/sqlite/local_db_adapter.d.ts +2 -0
  211. package/dist/repositories/sqlite/local_db_adapter.d.ts.map +1 -1
  212. package/dist/repositories/sqlite/local_db_adapter.js +21 -5
  213. package/dist/repositories/sqlite/local_db_adapter.js.map +1 -1
  214. package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
  215. package/dist/repositories/sqlite/sqlite_client.js +2 -0
  216. package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
  217. package/dist/repositories/sqlite/sqlite_driver.d.ts +10 -2
  218. package/dist/repositories/sqlite/sqlite_driver.d.ts.map +1 -1
  219. package/dist/repositories/sqlite/sqlite_driver.js +32 -5
  220. package/dist/repositories/sqlite/sqlite_driver.js.map +1 -1
  221. package/dist/repositories/worker/worker_file_database.d.ts +19 -0
  222. package/dist/repositories/worker/worker_file_database.d.ts.map +1 -1
  223. package/dist/repositories/worker/worker_file_database.js +23 -0
  224. package/dist/repositories/worker/worker_file_database.js.map +1 -1
  225. package/dist/server.d.ts +24 -1
  226. package/dist/server.d.ts.map +1 -1
  227. package/dist/server.js +658 -144
  228. package/dist/server.js.map +1 -1
  229. package/dist/services/agent_capabilities.d.ts +19 -4
  230. package/dist/services/agent_capabilities.d.ts.map +1 -1
  231. package/dist/services/agent_capabilities.js +60 -0
  232. package/dist/services/agent_capabilities.js.map +1 -1
  233. package/dist/services/agent_grants.d.ts.map +1 -1
  234. package/dist/services/agent_grants.js +19 -0
  235. package/dist/services/agent_grants.js.map +1 -1
  236. package/dist/services/array_item_patch.d.ts +158 -0
  237. package/dist/services/array_item_patch.d.ts.map +1 -0
  238. package/dist/services/array_item_patch.js +375 -0
  239. package/dist/services/array_item_patch.js.map +1 -0
  240. package/dist/services/attachment_resolution.d.ts.map +1 -1
  241. package/dist/services/attachment_resolution.js +2 -0
  242. package/dist/services/attachment_resolution.js.map +1 -1
  243. package/dist/services/attribution_redaction.d.ts.map +1 -1
  244. package/dist/services/attribution_redaction.js +16 -2
  245. package/dist/services/attribution_redaction.js.map +1 -1
  246. package/dist/services/batch_correction.d.ts +2 -0
  247. package/dist/services/batch_correction.d.ts.map +1 -1
  248. package/dist/services/batch_correction.js +19 -15
  249. package/dist/services/batch_correction.js.map +1 -1
  250. package/dist/services/canonical_markdown.d.ts +7 -23
  251. package/dist/services/canonical_markdown.d.ts.map +1 -1
  252. package/dist/services/canonical_markdown.js +10 -0
  253. package/dist/services/canonical_markdown.js.map +1 -1
  254. package/dist/services/correction.d.ts +118 -0
  255. package/dist/services/correction.d.ts.map +1 -1
  256. package/dist/services/correction.js +298 -36
  257. package/dist/services/correction.js.map +1 -1
  258. package/dist/services/correction_transaction.d.ts +27 -0
  259. package/dist/services/correction_transaction.d.ts.map +1 -0
  260. package/dist/services/correction_transaction.js +189 -0
  261. package/dist/services/correction_transaction.js.map +1 -0
  262. package/dist/services/dashboard_stats.d.ts.map +1 -1
  263. package/dist/services/dashboard_stats.js +8 -3
  264. package/dist/services/dashboard_stats.js.map +1 -1
  265. package/dist/services/deletion.d.ts.map +1 -1
  266. package/dist/services/deletion.js +235 -101
  267. package/dist/services/deletion.js.map +1 -1
  268. package/dist/services/entity_lifecycle_authority.d.ts +62 -0
  269. package/dist/services/entity_lifecycle_authority.d.ts.map +1 -0
  270. package/dist/services/entity_lifecycle_authority.js +210 -0
  271. package/dist/services/entity_lifecycle_authority.js.map +1 -0
  272. package/dist/services/entity_lifecycle_ingress.d.ts +15 -0
  273. package/dist/services/entity_lifecycle_ingress.d.ts.map +1 -0
  274. package/dist/services/entity_lifecycle_ingress.js +72 -0
  275. package/dist/services/entity_lifecycle_ingress.js.map +1 -0
  276. package/dist/services/entity_lifecycle_storage.d.ts +40 -0
  277. package/dist/services/entity_lifecycle_storage.d.ts.map +1 -0
  278. package/dist/services/entity_lifecycle_storage.js +415 -0
  279. package/dist/services/entity_lifecycle_storage.js.map +1 -0
  280. package/dist/services/entity_queries.d.ts +14 -1
  281. package/dist/services/entity_queries.d.ts.map +1 -1
  282. package/dist/services/entity_queries.js +117 -70
  283. package/dist/services/entity_queries.js.map +1 -1
  284. package/dist/services/entity_resolution.d.ts +24 -0
  285. package/dist/services/entity_resolution.d.ts.map +1 -1
  286. package/dist/services/entity_resolution.js +60 -4
  287. package/dist/services/entity_resolution.js.map +1 -1
  288. package/dist/services/entity_semantic_search.d.ts +3 -0
  289. package/dist/services/entity_semantic_search.d.ts.map +1 -1
  290. package/dist/services/entity_semantic_search.js +12 -4
  291. package/dist/services/entity_semantic_search.js.map +1 -1
  292. package/dist/services/entity_snapshot_at_time.d.ts +3 -1
  293. package/dist/services/entity_snapshot_at_time.d.ts.map +1 -1
  294. package/dist/services/entity_snapshot_at_time.js +71 -16
  295. package/dist/services/entity_snapshot_at_time.js.map +1 -1
  296. package/dist/services/entity_snapshot_embedding.d.ts +8 -1
  297. package/dist/services/entity_snapshot_embedding.d.ts.map +1 -1
  298. package/dist/services/entity_snapshot_embedding.js +60 -3
  299. package/dist/services/entity_snapshot_embedding.js.map +1 -1
  300. package/dist/services/entity_split.d.ts.map +1 -1
  301. package/dist/services/entity_split.js +4 -2
  302. package/dist/services/entity_split.js.map +1 -1
  303. package/dist/services/entity_version.d.ts +12 -0
  304. package/dist/services/entity_version.d.ts.map +1 -0
  305. package/dist/services/entity_version.js +17 -0
  306. package/dist/services/entity_version.js.map +1 -0
  307. package/dist/services/instance_policy.d.ts +3 -1
  308. package/dist/services/instance_policy.d.ts.map +1 -1
  309. package/dist/services/instance_policy.js +16 -2
  310. package/dist/services/instance_policy.js.map +1 -1
  311. package/dist/services/interpretation.d.ts +5 -0
  312. package/dist/services/interpretation.d.ts.map +1 -1
  313. package/dist/services/interpretation.js +3 -1
  314. package/dist/services/interpretation.js.map +1 -1
  315. package/dist/services/issues/config.d.ts.map +1 -1
  316. package/dist/services/issues/config.js +6 -0
  317. package/dist/services/issues/config.js.map +1 -1
  318. package/dist/services/issues/github_client.d.ts +7 -0
  319. package/dist/services/issues/github_client.d.ts.map +1 -1
  320. package/dist/services/issues/github_client.js +15 -3
  321. package/dist/services/issues/github_client.js.map +1 -1
  322. package/dist/services/issues/inspector_bulk.d.ts +2 -1
  323. package/dist/services/issues/inspector_bulk.d.ts.map +1 -1
  324. package/dist/services/issues/inspector_bulk.js +23 -5
  325. package/dist/services/issues/inspector_bulk.js.map +1 -1
  326. package/dist/services/issues/issue_operations.d.ts +10 -1
  327. package/dist/services/issues/issue_operations.d.ts.map +1 -1
  328. package/dist/services/issues/issue_operations.js +53 -21
  329. package/dist/services/issues/issue_operations.js.map +1 -1
  330. package/dist/services/issues/repo_allowlist.d.ts +55 -0
  331. package/dist/services/issues/repo_allowlist.d.ts.map +1 -0
  332. package/dist/services/issues/repo_allowlist.js +101 -0
  333. package/dist/services/issues/repo_allowlist.js.map +1 -0
  334. package/dist/services/issues/sync_issues_from_github.d.ts +83 -4
  335. package/dist/services/issues/sync_issues_from_github.d.ts.map +1 -1
  336. package/dist/services/issues/sync_issues_from_github.js +203 -28
  337. package/dist/services/issues/sync_issues_from_github.js.map +1 -1
  338. package/dist/services/issues/types.d.ts +21 -2
  339. package/dist/services/issues/types.d.ts.map +1 -1
  340. package/dist/services/issues/types.js.map +1 -1
  341. package/dist/services/local_entity_embedding.d.ts +3 -0
  342. package/dist/services/local_entity_embedding.d.ts.map +1 -1
  343. package/dist/services/local_entity_embedding.js +14 -2
  344. package/dist/services/local_entity_embedding.js.map +1 -1
  345. package/dist/services/memory_export.d.ts.map +1 -1
  346. package/dist/services/memory_export.js +2 -1
  347. package/dist/services/memory_export.js.map +1 -1
  348. package/dist/services/observation_insert.d.ts +137 -0
  349. package/dist/services/observation_insert.d.ts.map +1 -0
  350. package/dist/services/observation_insert.js +124 -0
  351. package/dist/services/observation_insert.js.map +1 -0
  352. package/dist/services/observation_source_rank.d.ts +29 -0
  353. package/dist/services/observation_source_rank.d.ts.map +1 -0
  354. package/dist/services/observation_source_rank.js +37 -0
  355. package/dist/services/observation_source_rank.js.map +1 -0
  356. package/dist/services/observation_source_warning.d.ts +66 -0
  357. package/dist/services/observation_source_warning.d.ts.map +1 -0
  358. package/dist/services/observation_source_warning.js +71 -0
  359. package/dist/services/observation_source_warning.js.map +1 -0
  360. package/dist/services/observation_storage.d.ts +16 -11
  361. package/dist/services/observation_storage.d.ts.map +1 -1
  362. package/dist/services/observation_storage.js +25 -38
  363. package/dist/services/observation_storage.js.map +1 -1
  364. package/dist/services/provenance_data_origin.d.ts +81 -0
  365. package/dist/services/provenance_data_origin.d.ts.map +1 -0
  366. package/dist/services/provenance_data_origin.js +171 -0
  367. package/dist/services/provenance_data_origin.js.map +1 -0
  368. package/dist/services/raw_fragments.d.ts +2 -0
  369. package/dist/services/raw_fragments.d.ts.map +1 -1
  370. package/dist/services/raw_fragments.js +20 -4
  371. package/dist/services/raw_fragments.js.map +1 -1
  372. package/dist/services/raw_storage.d.ts.map +1 -1
  373. package/dist/services/raw_storage.js +3 -0
  374. package/dist/services/raw_storage.js.map +1 -1
  375. package/dist/services/relationship_write_capability.d.ts +85 -0
  376. package/dist/services/relationship_write_capability.d.ts.map +1 -0
  377. package/dist/services/relationship_write_capability.js +139 -0
  378. package/dist/services/relationship_write_capability.js.map +1 -0
  379. package/dist/services/relationships.d.ts.map +1 -1
  380. package/dist/services/relationships.js +20 -0
  381. package/dist/services/relationships.js.map +1 -1
  382. package/dist/services/root_landing/site_nav.d.ts.map +1 -1
  383. package/dist/services/root_landing/site_nav.js +1 -18
  384. package/dist/services/root_landing/site_nav.js.map +1 -1
  385. package/dist/services/schema_reference_linking.d.ts.map +1 -1
  386. package/dist/services/schema_reference_linking.js +39 -0
  387. package/dist/services/schema_reference_linking.js.map +1 -1
  388. package/dist/services/schema_registry.d.ts +81 -6
  389. package/dist/services/schema_registry.d.ts.map +1 -1
  390. package/dist/services/schema_registry.js +235 -66
  391. package/dist/services/schema_registry.js.map +1 -1
  392. package/dist/services/snapshot_computation.d.ts +6 -3
  393. package/dist/services/snapshot_computation.d.ts.map +1 -1
  394. package/dist/services/snapshot_computation.js +40 -11
  395. package/dist/services/snapshot_computation.js.map +1 -1
  396. package/dist/services/snapshot_export.d.ts.map +1 -1
  397. package/dist/services/snapshot_export.js +2 -1
  398. package/dist/services/snapshot_export.js.map +1 -1
  399. package/dist/services/stable_serialize.d.ts +19 -0
  400. package/dist/services/stable_serialize.d.ts.map +1 -0
  401. package/dist/services/stable_serialize.js +40 -0
  402. package/dist/services/stable_serialize.js.map +1 -0
  403. package/dist/services/store_admission.d.ts +34 -0
  404. package/dist/services/store_admission.d.ts.map +1 -0
  405. package/dist/services/store_admission.js +155 -0
  406. package/dist/services/store_admission.js.map +1 -0
  407. package/dist/services/store_combined_admission.d.ts +7 -0
  408. package/dist/services/store_combined_admission.d.ts.map +1 -0
  409. package/dist/services/store_combined_admission.js +59 -0
  410. package/dist/services/store_combined_admission.js.map +1 -0
  411. package/dist/services/store_condition_identity.d.ts +11 -0
  412. package/dist/services/store_condition_identity.d.ts.map +1 -0
  413. package/dist/services/store_condition_identity.js +44 -0
  414. package/dist/services/store_condition_identity.js.map +1 -0
  415. package/dist/services/store_condition_keys.d.ts +72 -0
  416. package/dist/services/store_condition_keys.d.ts.map +1 -0
  417. package/dist/services/store_condition_keys.js +249 -0
  418. package/dist/services/store_condition_keys.js.map +1 -0
  419. package/dist/services/store_condition_request.d.ts +2 -0
  420. package/dist/services/store_condition_request.d.ts.map +1 -0
  421. package/dist/services/store_condition_request.js +39 -0
  422. package/dist/services/store_condition_request.js.map +1 -0
  423. package/dist/services/store_conditional.d.ts +50 -0
  424. package/dist/services/store_conditional.d.ts.map +1 -0
  425. package/dist/services/store_conditional.js +392 -0
  426. package/dist/services/store_conditional.js.map +1 -0
  427. package/dist/services/store_mutation_authority.d.ts +4 -0
  428. package/dist/services/store_mutation_authority.d.ts.map +1 -0
  429. package/dist/services/store_mutation_authority.js +42 -0
  430. package/dist/services/store_mutation_authority.js.map +1 -0
  431. package/dist/services/store_ownership_admission.d.ts +3 -0
  432. package/dist/services/store_ownership_admission.d.ts.map +1 -0
  433. package/dist/services/store_ownership_admission.js +60 -0
  434. package/dist/services/store_ownership_admission.js.map +1 -0
  435. package/dist/services/store_write_state.d.ts +112 -0
  436. package/dist/services/store_write_state.d.ts.map +1 -0
  437. package/dist/services/store_write_state.js +254 -0
  438. package/dist/services/store_write_state.js.map +1 -0
  439. package/dist/services/sync/peer_sync_batch.d.ts.map +1 -1
  440. package/dist/services/sync/peer_sync_batch.js +2 -1
  441. package/dist/services/sync/peer_sync_batch.js.map +1 -1
  442. package/dist/services/timeline_events.d.ts +2 -0
  443. package/dist/services/timeline_events.d.ts.map +1 -1
  444. package/dist/services/timeline_events.js +2 -0
  445. package/dist/services/timeline_events.js.map +1 -1
  446. package/dist/shared/action_handlers/entity_handlers.d.ts +3 -0
  447. package/dist/shared/action_handlers/entity_handlers.d.ts.map +1 -1
  448. package/dist/shared/action_handlers/entity_handlers.js +280 -15
  449. package/dist/shared/action_handlers/entity_handlers.js.map +1 -1
  450. package/dist/shared/action_schemas.d.ts +709 -232
  451. package/dist/shared/action_schemas.d.ts.map +1 -1
  452. package/dist/shared/action_schemas.js +274 -11
  453. package/dist/shared/action_schemas.js.map +1 -1
  454. package/dist/shared/bind_host.d.ts +28 -0
  455. package/dist/shared/bind_host.d.ts.map +1 -0
  456. package/dist/shared/bind_host.js +34 -0
  457. package/dist/shared/bind_host.js.map +1 -0
  458. package/dist/shared/contract_mappings.d.ts.map +1 -1
  459. package/dist/shared/contract_mappings.js +27 -0
  460. package/dist/shared/contract_mappings.js.map +1 -1
  461. package/dist/shared/entity_read_contract.d.ts +55 -0
  462. package/dist/shared/entity_read_contract.d.ts.map +1 -0
  463. package/dist/shared/entity_read_contract.js +201 -0
  464. package/dist/shared/entity_read_contract.js.map +1 -0
  465. package/dist/shared/openapi_types.d.ts +1073 -40
  466. package/dist/shared/openapi_types.d.ts.map +1 -1
  467. package/dist/shared/repo_slug.d.ts +27 -0
  468. package/dist/shared/repo_slug.d.ts.map +1 -0
  469. package/dist/shared/repo_slug.js +45 -0
  470. package/dist/shared/repo_slug.js.map +1 -0
  471. package/dist/shared/skill_deprecation.d.ts +26 -0
  472. package/dist/shared/skill_deprecation.d.ts.map +1 -0
  473. package/dist/shared/skill_deprecation.js +41 -0
  474. package/dist/shared/skill_deprecation.js.map +1 -0
  475. package/dist/tool_definitions.d.ts +1 -1
  476. package/dist/tool_definitions.d.ts.map +1 -1
  477. package/dist/tool_definitions.js +57 -12
  478. package/dist/tool_definitions.js.map +1 -1
  479. package/dist/utils/lucide_icons.d.ts +1 -1
  480. package/dist/utils/safe_request_log_format.d.ts +2 -0
  481. package/dist/utils/safe_request_log_format.d.ts.map +1 -1
  482. package/dist/utils/safe_request_log_format.js +53 -0
  483. package/dist/utils/safe_request_log_format.js.map +1 -1
  484. package/docs/README.md +49 -0
  485. package/docs/developer/mcp/instructions.md +34 -23
  486. package/openapi.yaml +1424 -34
  487. package/package.json +10 -2
  488. package/skills/end/SKILL.md +2 -2
  489. package/skills/ensure-neotoma/SKILL.md +4 -2
  490. package/skills/query-memory/SKILL.md +13 -2
  491. package/skills/recover-sqlite-database/SKILL.md +1 -1
  492. package/skills/store-data/SKILL.md +12 -2
  493. package/dist/inspector/assets/agent_grant_form-DoLjngLK.js +0 -1
  494. package/dist/inspector/assets/interpretations-CJF4giZE.js +0 -1
  495. package/dist/inspector/assets/record_activity-Bcv9OocC.js +0 -1
  496. package/dist/inspector/assets/use_interpretations-L8TsWYxH.js +0 -1
package/dist/actions.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { preflightStructuredStoreAdmission, } from "./services/store_admission.js";
1
2
  import express from "express";
2
3
  import cookieParser from "cookie-parser";
3
4
  import cors from "cors";
@@ -35,14 +36,14 @@ import { OwnedEntityNotFoundError, SOURCES_STORAGE_BUCKET } from "./services/sco
35
36
  import { invalidEntityIdRelationshipRefusal, relationshipRefusalFromError, unresolvedRelationshipRefusal, } from "./services/store_relationships.js";
36
37
  import { CursorError } from "./services/entity_cursor.js";
37
38
  import { assertNoShadowedRoutes } from "./services/route_shadowing.js";
38
- import { StorePolicyUnavailableError } from "./services/instance_policy.js";
39
+ import { StorePolicyDeniedError, StorePolicyUnavailableError } from "./services/instance_policy.js";
39
40
  import { AgentCapabilityError, contextFromAgentIdentity, enforceAgentCapability, } from "./services/agent_capabilities.js";
40
- import { assertGuestWriteAllowed, AccessPolicyError, } from "./services/access_policy.js";
41
+ import { AccessPolicyError } from "./services/access_policy.js";
41
42
  import { hashGuestAccessToken } from "./services/guest_access_token.js";
42
43
  import { subscriptionWithinGuestScope } from "./services/subscriptions/guest_scope.js";
43
44
  import { IssueTransportError, IssueValidationError } from "./services/issues/errors.js";
44
45
  import { externalActorFromCallerInput } from "./services/issues/external_actor_builder.js";
45
- import { getCurrentAAuthAdmission, getCurrentAgentIdentity, getCurrentAttribution, getCurrentExternalActor, getRequestContext, runWithAuthenticatedPrincipal, runWithExternalActor, runWithRequestContext, } from "./services/request_context.js";
46
+ import { getCurrentAAuthAdmission, getCurrentAgentIdentity, getCurrentAttribution, getCurrentExternalActor, runWithAuthenticatedPrincipal, runWithExternalActor, runWithRequestContext, } from "./services/request_context.js";
46
47
  import { assertCanWriteProtectedBatch } from "./services/protected_entity_types.js";
47
48
  import { redactMemberAttribution } from "./services/attribution_redaction.js";
48
49
  import { createAgentIdentity as buildAgentIdentity, getAgentIdentityFromRequest, normaliseClientName, } from "./crypto/agent_identity.js";
@@ -74,7 +75,8 @@ import { getDb } from "./repositories/db/connection.js";
74
75
  import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
75
76
  import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
76
77
  import { buildGoogleAuthorizeUrl, createGoogleSigninNonce, consumeGoogleSigninNonce, exchangeGoogleAuthorizationCode, getSharedGraphUserId, GoogleIdTokenError, isGoogleSigninEnabled, verifyGoogleIdToken, } from "./services/google_oidc.js";
77
- import { AnalyzeSchemaCandidatesRequestSchema, AuditUndeclaredFragmentsRequestSchema, CorrectEntityRequestSchema, CreateInterpretationRequestSchema, CreateRelationshipsRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, IssuesBulkEntityIdsRequestSchema, IssuesAddMessageRequestSchema, IssuesGetStatusRequestSchema, IssuesSubmitRequestSchema, IssuesSyncRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, RELATIONSHIP_ENTITY_ID_FORMAT_HINT, RELATIONSHIP_ENTITY_ID_FORMAT_ISSUE_CODE, StoreRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
78
+ import { AnalyzeSchemaCandidatesRequestSchema, AuditUndeclaredFragmentsRequestSchema, CorrectEntityRequestSchema, PatchArrayItemRequestSchema, CreateInterpretationRequestSchema, CreateRelationshipsRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, IssuesBulkEntityIdsRequestSchema, IssuesAddMessageRequestSchema, IssuesGetStatusRequestSchema, IssuesSubmitRequestSchema, IssuesSyncRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, RELATIONSHIP_ENTITY_ID_FORMAT_HINT, RELATIONSHIP_ENTITY_ID_FORMAT_ISSUE_CODE, ObservationSourceSchema, StoreRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
79
+ import { DEFAULT_OBSERVATION_SOURCE } from "./services/observation_insert.js";
78
80
  import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
79
81
  import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
80
82
  import { retrieveEntityByIdentifierWithFallback } from "./shared/action_handlers/entity_identifier_handler.js";
@@ -92,6 +94,7 @@ import { buildComplianceScorecard } from "./services/compliance/scorecard.js";
92
94
  import { getAgent, listAgentRecords, listAgents } from "./services/agents_directory.js";
93
95
  import { computeEntitySnapshotAtTime } from "./services/entity_snapshot_at_time.js";
94
96
  import { isProductionEnvironment } from "./shared/environment.js";
97
+ import { isLoopbackHost, resolveBindHostFromEnv } from "./shared/bind_host.js";
95
98
  // Set to true during startHTTPServer() when resolveSandboxMode() returns
96
99
  // "local_sandbox". Read by getAuthenticatedUserId() to substitute the
97
100
  // per-install fingerprinted sandbox principal for the shared nil UUID fallback.
@@ -455,21 +458,25 @@ function isInternalSeedRequest(req) {
455
458
  // available so a single abusive client does not starve others. Operators
456
459
  // can tune with NEOTOMA_WRITE_RATE_LIMIT_PER_MIN.
457
460
  const WRITE_RATE_LIMIT_PER_MIN = Math.max(1, Number.parseInt(process.env.NEOTOMA_WRITE_RATE_LIMIT_PER_MIN || "", 10) || 120);
458
- const writeRateLimit = rateLimit({
459
- windowMs: 60 * 1000,
460
- max: WRITE_RATE_LIMIT_PER_MIN,
461
- keyGenerator: (req) => {
462
- const userId = req.authenticatedUserId || "";
463
- if (userId)
464
- return `u:${userId}`;
465
- // Use the library-provided IPv6-safe key helper; keying directly on
466
- // req.ip lets IPv6 callers rotate the low-64 bits to bypass limits.
467
- return `ip:${ipKeyGenerator(req.ip || "")}`;
468
- },
469
- skip: isInternalSeedRequest,
470
- message: "Write rate limit exceeded, please slow down",
471
- ...rateLimitOptions,
472
- });
461
+ /** Build the shared write limiter; exported so its limiting effect is testable without a listener. */
462
+ export function createWriteRateLimit(max = WRITE_RATE_LIMIT_PER_MIN) {
463
+ return rateLimit({
464
+ windowMs: 60 * 1000,
465
+ max,
466
+ keyGenerator: (req) => {
467
+ const userId = req.authenticatedUserId || "";
468
+ if (userId)
469
+ return `u:${userId}`;
470
+ // Use the library-provided IPv6-safe key helper; keying directly on
471
+ // req.ip lets IPv6 callers rotate the low-64 bits to bypass limits.
472
+ return `ip:${ipKeyGenerator(req.ip || "")}`;
473
+ },
474
+ skip: isInternalSeedRequest,
475
+ message: "Write rate limit exceeded, please slow down",
476
+ ...rateLimitOptions,
477
+ });
478
+ }
479
+ const writeRateLimit = createWriteRateLimit();
473
480
  // SECURITY: guest-capable write routes (issue submission / thread append,
474
481
  // subscribe / unsubscribe) should not share the broader `/store` bucket.
475
482
  // Key by cryptographic guest identity first, then scoped guest token, then IP.
@@ -4163,6 +4170,13 @@ function handleApiError(req, res, error, defaultMessage, errorCode = "DB_QUERY_F
4163
4170
  .status(error.statusCode)
4164
4171
  .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
4165
4172
  }
4173
+ if (error instanceof StorePolicyDeniedError) {
4174
+ logWarn(logContext || "StorePolicyDenied", req, {
4175
+ denied_count: error.denied.length,
4176
+ reason_codes: [...new Set(error.denied.map((denial) => denial.reason_code))].sort(),
4177
+ });
4178
+ return res.status(400).json({ error: error.toErrorEnvelope() });
4179
+ }
4166
4180
  if (error instanceof StorePolicyUnavailableError) {
4167
4181
  // 503, not 400: the write was refused because the instance could not read
4168
4182
  // its own policy, which is a service-side fault the caller can retry — not
@@ -4266,7 +4280,12 @@ async function runEntitiesQuery(req, res, rawInput) {
4266
4280
  // the request for access control.
4267
4281
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
4268
4282
  const { entity_type, entity_types, search, limit, offset, cursor, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
4269
- const { entities, total, applied_search_strategies, search_mode, next_cursor } = await queryEntitiesWithCount({
4283
+ const { entities, total, applied_search_strategies, search_mode, next_cursor, read_contract } = await queryEntitiesWithCount({
4284
+ readRequest: {
4285
+ raw: (req.method === "GET" ? req.query : rawInput),
4286
+ surface: req.method === "GET" ? "http_get" : "http_post",
4287
+ consumed: [...Object.keys(parsed.data), "query", "search_query"],
4288
+ },
4270
4289
  userId,
4271
4290
  entityType: entity_type,
4272
4291
  entityTypes: entity_types,
@@ -4292,6 +4311,7 @@ async function runEntitiesQuery(req, res, rawInput) {
4292
4311
  total,
4293
4312
  limit,
4294
4313
  offset,
4314
+ read_contract,
4295
4315
  ...(applied_search_strategies ? { applied_search_strategies } : {}),
4296
4316
  search_mode,
4297
4317
  ...(next_cursor ? { next_cursor } : {}),
@@ -6259,9 +6279,9 @@ app.post("/observations/create", async (req, res) => {
6259
6279
  entity_identifier: z.string(),
6260
6280
  fields: z.record(z.unknown()),
6261
6281
  source_priority: z.number().optional().default(100),
6262
- observation_source: z
6263
- .enum(["sensor", "llm_summary", "workflow_state", "human", "import", "sync"])
6264
- .optional(),
6282
+ // Shared enum (#2241): an inline copy here had drifted from the store
6283
+ // schema's once already; reuse the single source instead.
6284
+ observation_source: ObservationSourceSchema.optional(),
6265
6285
  source_peer_id: z.string().min(1).optional(),
6266
6286
  user_id: z.string().uuid(),
6267
6287
  });
@@ -6357,143 +6377,53 @@ async function completeInterpretationRun(params) {
6357
6377
  })
6358
6378
  .eq("id", params.interpretationId);
6359
6379
  }
6380
+ /**
6381
+ * Structured store entry point.
6382
+ *
6383
+ * A committing store is a write: once it starts it runs to completion and is
6384
+ * not cancelled by the caller's abort signal, and a second call for the same
6385
+ * idempotency key waits for the first (#2144). See `runKeyedStoreWrite`.
6386
+ * Plan mode (`commit: false`) writes nothing and the conditional path is its
6387
+ * own atomic transaction, so both run as before.
6388
+ */
6360
6389
  export async function storeStructuredForApi(params) {
6361
- const { userId, entities, sourcePriority, observationSource, sourcePeerId, idempotencyKey, originalFilename, relationships, interpretation, interpretationSourceId, commit: commitInput, strict: strictInput, } = params;
6390
+ if (params.commit === false || params.expectedEntityAbsent === true) {
6391
+ return storeStructuredForApiUnguarded(params);
6392
+ }
6393
+ const { runKeyedStoreWrite } = await import("./services/store_write_state.js");
6394
+ return runKeyedStoreWrite(params.userId, params.idempotencyKey, () => storeStructuredForApiUnguarded(params));
6395
+ }
6396
+ async function storeStructuredForApiUnguarded(params) {
6397
+ const { userId, entities, sourcePriority, observationSource, sourcePeerId, idempotencyKey, originalFilename, relationships, interpretation, interpretationSourceId, commit: commitInput, strict: strictInput, dataOrigins, } = params;
6362
6398
  const commit = commitInput !== false;
6363
6399
  const strict = strictInput === true;
6364
- // Access policy: when the caller is a guest (AAuth-verified but not admitted
6365
- // via a grant), check per-entity-type access policies. If the policy allows
6366
- // guest writes, the request proceeds without requiring an agent_grant.
6367
- const requestContext = getRequestContext();
6368
- const agentIdentity = getCurrentAgentIdentity();
6369
- const admission = getCurrentAAuthAdmission();
6370
- const isAAuthVerified = agentIdentity?.thumbprint != null;
6371
- const isGuest = isAAuthVerified && (!admission || !admission.admitted);
6372
- if (isGuest && !requestContext?.bypassGuestStoreAccessPolicy) {
6373
- const entityTypes = entities
6374
- .map((entity) => entity?.entity_type)
6375
- .filter((t) => typeof t === "string" && t.length > 0);
6376
- const guestId = {
6377
- thumbprint: agentIdentity.thumbprint,
6378
- sub: agentIdentity.sub,
6379
- iss: agentIdentity.iss,
6380
- };
6381
- await assertGuestWriteAllowed(entityTypes, guestId);
6382
- }
6383
- // usage_digest store-seam redaction guard (server-side backstop).
6384
- // Scope: strictly usage_digest entities only — no impact on any other entity type.
6385
- // For each incoming usage_digest entity, scan free-text fields `notes` and
6386
- // `friction_notes` via the pure redactUsageDigestEntity helper, and write the
6387
- // redacted copies back onto the entity object so the raw input is never persisted.
6388
- // This is a redact-and-store (scan) approach, not hard-reject, so a digest is
6389
- // never silently dropped on a client-side redaction miss.
6390
- //
6391
- // NOTE: Single telemetry sink today — entity_type === "usage_digest" is the only
6392
- // branch. If a second redacted-free-text entity_type appears, lift these field
6393
- // names into schema metadata (redact_free_text_fields) rather than adding another
6394
- // branch here.
6395
- {
6396
- const hasUsageDigest = entities.some((e) => e?.entity_type === "usage_digest");
6397
- if (hasUsageDigest) {
6398
- const { redactUsageDigestEntity } = await import("./services/feedback/usage_digest_redaction.js");
6399
- for (const entityData of entities) {
6400
- const ed = entityData;
6401
- if (ed.entity_type !== "usage_digest")
6402
- continue;
6403
- const result = redactUsageDigestEntity(ed);
6404
- if (result.applied) {
6405
- logger.warn(`[STORE] usage_digest server-side redaction applied: ${result.hits} hit(s) in free-text fields.`);
6406
- }
6407
- }
6408
- }
6409
- }
6410
- // Capability scoping: when the caller is an AAuth-verified agent covered
6411
- // by the capability registry, gate store_structured by entity_type here
6412
- // before any writes touch the DB. Guests who passed access policy above
6413
- // skip grant-based capability enforcement. Unknown / anonymous callers
6414
- // fall through (attribution_policy still gates their writes downstream).
6415
- const capabilityCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
6416
- if (capabilityCtx && !isGuest) {
6417
- const entityTypes = entities
6418
- .map((entity) => entity?.entity_type)
6419
- .filter((t) => typeof t === "string" && t.length > 0);
6420
- enforceAgentCapability("store", entityTypes, capabilityCtx);
6421
- const relationshipOp = Array.isArray(relationships) && relationships.length > 0;
6422
- if (relationshipOp) {
6423
- enforceAgentCapability("create_relationship", entityTypes, capabilityCtx);
6424
- }
6425
- }
6426
- // Relationship-type validation, UP FRONT and BEFORE any entity is persisted
6427
- // (#1972 / G25).
6428
- //
6429
- // This closes the more damaging half of the closed-vocabulary defect. The
6430
- // relationships leg further down catches per edge and downgrades to
6431
- // logger.warn, so a store carrying an edge the substrate would not accept
6432
- // returned SUCCESS with the edge silently absent — a caller believed it had
6433
- // written a graph it had not written, and nothing in the response said
6434
- // otherwise. Validating here means an unacceptable type is a refusal the
6435
- // caller sees, and no entities are written that would have been orphaned by
6436
- // the edge that was going to fail anyway.
6437
- if (commit && Array.isArray(relationships) && relationships.length > 0) {
6438
- const { relationshipsService } = await import("./services/relationships.js");
6439
- const seen = new Set();
6440
- for (const rel of relationships) {
6441
- const type = rel?.relationship_type;
6442
- if (typeof type !== "string" || seen.has(type))
6443
- continue;
6444
- seen.add(type);
6445
- await relationshipsService.assertRegisteredType(type, userId);
6446
- }
6447
- }
6448
- // Protected-entity-types guard: governance state (`agent_grant`, etc.)
6449
- // is gated by an explicit capability on the admitted grant. Mirrors
6450
- // the same check made deep in `createObservation` so callers see a
6451
- // structured `capability_denied` envelope before any writes.
6452
- {
6453
- const entityTypes = entities
6454
- .map((entity) => entity?.entity_type)
6455
- .filter((t) => typeof t === "string" && t.length > 0);
6456
- assertCanWriteProtectedBatch({
6457
- entity_types: entityTypes,
6458
- op: "store",
6459
- identity: getCurrentAgentIdentity(),
6460
- admission: getCurrentAAuthAdmission(),
6461
- });
6462
- }
6463
- // A key thumbprint may be pinned by agent_grants under one owner only.
6464
- // Checked before any write so a refused pin persists nothing; the same
6465
- // check runs again at the observation insert.
6466
- if (entities.some((entity) => entity?.entity_type === "agent_grant")) {
6467
- const { assertGrantWriteKeepsPinUnique } = await import("./services/agent_grants.js");
6468
- for (const entity of entities) {
6469
- if (entity?.entity_type !== "agent_grant")
6470
- continue;
6471
- await assertGrantWriteKeepsPinUnique({
6472
- userId,
6473
- entityType: "agent_grant",
6474
- fields: entity,
6475
- });
6476
- }
6477
- }
6400
+ await preflightStructuredStoreAdmission(params);
6478
6401
  const { resolveEntityWithTrace, CanonicalNameUnresolvedError, MergeRefusedError } = await import("./services/entity_resolution.js");
6479
- const { detectFlatPackedRows, FlatPackedRowsError } = await import("./services/flat_packed_detection.js");
6480
6402
  // R4: lazy import so the structured-store handler can attach required
6481
6403
  // identity-field hints when resolving fails under a reject policy.
6482
6404
  const registryModule = await import("./services/schema_registry.js");
6483
6405
  const deriveRequiredIdentityFields = registryModule.deriveRequiredIdentityFields;
6484
- // Reject flat-packed rows (whole tables smuggled into a single entity as
6485
- // `<prefix>_<index>_<suffix>` keys). These cannot produce per-row snapshots
6486
- // and are almost always a caller bug. The caller should split into one
6487
- // entity per row and retry.
6488
- for (const entityData of entities) {
6489
- const detection = detectFlatPackedRows(entityData);
6490
- if (detection.detected) {
6491
- throw new FlatPackedRowsError(detection);
6492
- }
6406
+ if (params.expectedEntityAbsent === true) {
6407
+ const { storeConditionalStructured } = await import("./services/store_conditional.js");
6408
+ return storeConditionalStructured({
6409
+ userId,
6410
+ entities,
6411
+ sourcePriority,
6412
+ observationSource,
6413
+ idempotencyKey,
6414
+ originalFilename,
6415
+ strict,
6416
+ dataOrigin: dataOrigins?.[0],
6417
+ });
6493
6418
  }
6494
6419
  // Compute hash of incoming content before querying so we can detect
6495
- // idempotency-key reuse with different content (issue #186).
6496
- const incomingJsonContent = JSON.stringify(entities, (key, value) => {
6420
+ // idempotency-key reuse with different content (issue #186). Each entity's
6421
+ // effective data_origin (#2242) is part of the stored payload, so a replay
6422
+ // with a different origin is a mismatch rather than a silent drop; with no
6423
+ // origin the stored JSON is unchanged.
6424
+ const { entitiesWithStoredDataOrigins } = await import("./services/provenance_data_origin.js");
6425
+ const storedEntities = entitiesWithStoredDataOrigins(entities, dataOrigins);
6426
+ const incomingJsonContent = JSON.stringify(storedEntities, (key, value) => {
6497
6427
  if (typeof value === "bigint") {
6498
6428
  return Number(value);
6499
6429
  }
@@ -6501,9 +6431,14 @@ export async function storeStructuredForApi(params) {
6501
6431
  });
6502
6432
  const { computeContentHash: _computeContentHash } = await import("./services/raw_storage.js");
6503
6433
  const incomingContentHash = _computeContentHash(Buffer.from(incomingJsonContent, "utf-8"));
6434
+ if (commit) {
6435
+ const { assertLegacyStoreKeyMode } = await import("./services/store_condition_keys.js");
6436
+ const { getDb } = await import("./repositories/db/connection.js");
6437
+ await assertLegacyStoreKeyMode(await getDb(), userId, idempotencyKey);
6438
+ }
6504
6439
  const { data: existingSource, error: existingSourceError } = await db
6505
6440
  .from("sources")
6506
- .select("id, content_hash")
6441
+ .select("id, content_hash, provenance")
6507
6442
  .eq("user_id", userId)
6508
6443
  .eq("idempotency_key", idempotencyKey)
6509
6444
  .maybeSingle();
@@ -6523,6 +6458,51 @@ export async function storeStructuredForApi(params) {
6523
6458
  "To update an entity, omit the idempotency_key or use a new one.";
6524
6459
  throw mismatchErr;
6525
6460
  }
6461
+ }
6462
+ // A recorded key is not a finished write (#2144): the source row is created
6463
+ // before any observation, so a loop that stopped part way leaves a key that
6464
+ // looks done. `resumeIncompleteWrite` is true when the earlier attempt did
6465
+ // not finish; the replay branch below is then skipped and the ordinary write
6466
+ // path runs again over the same source. That is safe because observation ids
6467
+ // are deterministic (source, entity, fields), so entities already written
6468
+ // resolve to their existing observation and only the missing ones are added.
6469
+ let resumeIncompleteWrite = false;
6470
+ if (existingSource && commit) {
6471
+ const storeWriteState = await import("./services/store_write_state.js");
6472
+ const recordedState = storeWriteState.readStoreWriteState(existingSource.provenance);
6473
+ const incomplete = recordedState === "in_progress" ||
6474
+ (recordedState === undefined &&
6475
+ storeWriteState.legacyWriteLooksIncomplete({
6476
+ entities,
6477
+ observationCount: (await (await import("./services/observation_storage.js")).listObservationsForSource(existingSource.id, userId)).length,
6478
+ observationsOnKeyedSource: !interpretation?.source_id && !interpretationSourceId,
6479
+ }));
6480
+ if (incomplete) {
6481
+ if (interpretation) {
6482
+ // A resumed write would open a fresh interpretation run, whose id is
6483
+ // part of each observation id, so the entities already written would
6484
+ // be written a second time. Report the incomplete write instead.
6485
+ // Count where the interrupted attempt wrote its observations: the
6486
+ // interpretation's own source when it names one (same resolution as
6487
+ // `observationSourceId` below and the MCP path), else the keyed source.
6488
+ const summary = await storeWriteState.summarizeUnfinishedStore({
6489
+ userId,
6490
+ sourceId: interpretation.source_id ?? interpretationSourceId ?? existingSource.id,
6491
+ entities: entities,
6492
+ });
6493
+ const incompleteErr = new Error(`ERR_STORE_WRITE_INCOMPLETE: idempotency_key "${idempotencyKey}" belongs to a store ` +
6494
+ `that did not finish writing (${summary.entities_written} of ` +
6495
+ `${summary.entities_requested} entities have an observation). It cannot be resumed ` +
6496
+ "under the same key because the request carries an interpretation.");
6497
+ incompleteErr.code = "ERR_STORE_WRITE_INCOMPLETE";
6498
+ incompleteErr.hint = storeWriteState.incompleteStoreHint(summary);
6499
+ incompleteErr.details = { ...summary };
6500
+ throw incompleteErr;
6501
+ }
6502
+ resumeIncompleteWrite = true;
6503
+ }
6504
+ }
6505
+ if (existingSource && !resumeIncompleteWrite) {
6526
6506
  const { listObservationsForSource } = await import("./services/observation_storage.js");
6527
6507
  const existingObs = await listObservationsForSource(existingSource.id, userId);
6528
6508
  // Issue #1860 (mirrors the #1840 MCP-path fix in src/server.ts): on a
@@ -6598,24 +6578,39 @@ export async function storeStructuredForApi(params) {
6598
6578
  return entry?.schema_definition ?? null;
6599
6579
  });
6600
6580
  }
6581
+ const { preflightStructuredStoreOwnership } = await import("./services/store_ownership_admission.js");
6582
+ if (commit)
6583
+ await preflightStructuredStoreOwnership(params);
6601
6584
  const { createObservation } = await import("./services/observation_storage.js");
6602
6585
  const jsonContent = incomingJsonContent;
6603
6586
  const filenameForStorage = originalFilename?.trim() || undefined;
6604
- const storageResult = await storeRawContent({
6605
- userId,
6606
- fileBuffer: Buffer.from(jsonContent, "utf-8"),
6607
- mimeType: "application/json",
6608
- originalFilename: filenameForStorage,
6609
- idempotencyKey,
6610
- provenance: {
6611
- upload_method: "api_store",
6612
- client: "api",
6613
- source_priority: sourcePriority,
6614
- },
6615
- });
6587
+ // Plan mode (`commit: false`, CLI `--plan`/`--dry-run`) must be genuinely
6588
+ // side-effect-free: no `sources` row, no raw-storage upload. storeRawContent
6589
+ // persists both, so it must not run at all when not committing. Everything
6590
+ // storageResult feeds below — resolvedInterpretationSourceId,
6591
+ // observationSourceId, and interpretationId — is consumed exclusively
6592
+ // inside `if (commit)` blocks further down (observation creation, event
6593
+ // emission, auto-linking), so a null placeholder here is never written to
6594
+ // a row; pass 1 entity resolution (resolveEntityWithTrace, below) takes no
6595
+ // source id at all.
6596
+ const storageResult = commit
6597
+ ? await storeRawContent({
6598
+ userId,
6599
+ fileBuffer: Buffer.from(jsonContent, "utf-8"),
6600
+ mimeType: "application/json",
6601
+ originalFilename: filenameForStorage,
6602
+ idempotencyKey,
6603
+ provenance: {
6604
+ upload_method: "api_store",
6605
+ client: "api",
6606
+ source_priority: sourcePriority,
6607
+ ...(await import("./services/store_write_state.js")).inProgressStoreWriteProvenance(),
6608
+ },
6609
+ })
6610
+ : null;
6616
6611
  const resolvedInterpretationSourceId = interpretation?.source_id ??
6617
6612
  interpretationSourceId ??
6618
- (interpretation?.source_ref === "structured" ? storageResult.sourceId : undefined);
6613
+ (interpretation?.source_ref === "structured" ? storageResult?.sourceId : undefined);
6619
6614
  const interpretationId = commit && interpretation && resolvedInterpretationSourceId
6620
6615
  ? await createInterpretationRunForSource({
6621
6616
  userId,
@@ -6623,7 +6618,7 @@ export async function storeStructuredForApi(params) {
6623
6618
  interpretationConfig: interpretation.interpretation_config,
6624
6619
  })
6625
6620
  : null;
6626
- const observationSourceId = resolvedInterpretationSourceId ?? storageResult.sourceId;
6621
+ const observationSourceId = resolvedInterpretationSourceId ?? storageResult?.sourceId;
6627
6622
  // v0.5.1: structured guidance for the v0.5.0 breaking change where callers
6628
6623
  // nested fields under `attributes`. If resolution failed and the only
6629
6624
  // observed top-level keys are `attributes` (plus optionally `entity_type`),
@@ -6644,6 +6639,11 @@ export async function storeStructuredForApi(params) {
6644
6639
  }
6645
6640
  const resolved = [];
6646
6641
  const issues = [];
6642
+ // One fresh set per resolution pass when resuming (#2144): an entity an
6643
+ // earlier payload of this request already resolved to is a collision between
6644
+ // two payloads, not this write's own row, so a later payload landing on it is
6645
+ // judged by the ordinary strict / collision rules.
6646
+ const resumeClaimedEntityIds = resumeIncompleteWrite ? new Set() : undefined;
6647
6647
  for (let observation_index = 0; observation_index < entities.length; observation_index++) {
6648
6648
  const entityData = entities[observation_index];
6649
6649
  let entity_type = entityData.entity_type;
@@ -6690,7 +6690,12 @@ export async function storeStructuredForApi(params) {
6690
6690
  commit,
6691
6691
  strict: effectiveStrict,
6692
6692
  targetId,
6693
+ // Resuming this key's own interrupted write: entity rows it created
6694
+ // are not merge targets (#2144).
6695
+ resumeSourceId: resumeIncompleteWrite ? existingSource?.id : undefined,
6696
+ resumeClaimedEntityIds,
6693
6697
  });
6698
+ resumeClaimedEntityIds?.add(result.entityId);
6694
6699
  resolved.push({
6695
6700
  observation_index,
6696
6701
  entity_type,
@@ -6947,6 +6952,15 @@ export async function storeStructuredForApi(params) {
6947
6952
  let observation_id = null;
6948
6953
  let snapshotAfter = null;
6949
6954
  if (commit) {
6955
+ // Invariant: storageResult is only ever null when `commit` is false
6956
+ // (plan mode skips storeRawContent entirely — see above). Reaching this
6957
+ // branch with a null observationSourceId would mean an observation is
6958
+ // about to be written with no traceable source, which is worse than
6959
+ // failing loudly here.
6960
+ if (observationSourceId === undefined) {
6961
+ throw new Error("Internal invariant violated: commit=true but no source id was resolved " +
6962
+ "(storeRawContent should have run under commit mode).");
6963
+ }
6950
6964
  const { data: priorSnapRow } = await db
6951
6965
  .from("entity_snapshots")
6952
6966
  .select("snapshot")
@@ -6970,6 +6984,7 @@ export async function storeStructuredForApi(params) {
6970
6984
  user_id: userId,
6971
6985
  identity_basis: r.trace.identity_basis,
6972
6986
  identity_rule: r.trace.identity_rule,
6987
+ data_origin: dataOrigins?.[r.observation_index],
6973
6988
  });
6974
6989
  observation_id = obsData.id;
6975
6990
  try {
@@ -6986,7 +7001,7 @@ export async function storeStructuredForApi(params) {
6986
7001
  const obsRow = obsData;
6987
7002
  const emitTs = obsRow.observed_at ?? new Date().toISOString();
6988
7003
  const obsSourceLabel = observationSource === undefined || observationSource === null
6989
- ? "llm_summary"
7004
+ ? DEFAULT_OBSERVATION_SOURCE
6990
7005
  : String(observationSource);
6991
7006
  emitObservationCreated({
6992
7007
  user_id: userId,
@@ -7154,6 +7169,14 @@ export async function storeStructuredForApi(params) {
7154
7169
  }
7155
7170
  }
7156
7171
  }
7172
+ // Every observation (and the request's relationships) is written. Record
7173
+ // that on the keyed source so a replay of this key knows the write finished
7174
+ // (#2144). Before this point a replay resumes the write instead of reporting
7175
+ // whatever happened to land.
7176
+ if (commit && storageResult) {
7177
+ const { markStoreWriteComplete } = await import("./services/store_write_state.js");
7178
+ await markStoreWriteComplete(storageResult.sourceId, userId);
7179
+ }
7157
7180
  // R3: aggregate non-fatal resolver warnings across the batch so callers
7158
7181
  // (agents, Inspector, CI) can audit identity quality without walking every
7159
7182
  // entity.trace. PII-free by construction — see ResolverWarning shape.
@@ -7385,6 +7408,20 @@ export async function storeStructuredForApi(params) {
7385
7408
  }
7386
7409
  }
7387
7410
  }
7411
+ // #2241: OBSERVATION_SOURCE_OMITTED — the request did not say what kind of
7412
+ // write this was, so every observation was stored as `unspecified`. Judged
7413
+ // on the raw request value (an explicit `unspecified` does not warn) and
7414
+ // emitted once per request, because observation_source is request-scoped.
7415
+ {
7416
+ const { buildObservationSourceOmittedWarning } = await import("./services/observation_source_warning.js");
7417
+ const firstStored = [...resolved].sort((a, b) => a.observation_index - b.observation_index)[0];
7418
+ const obsSourceWarn = buildObservationSourceOmittedWarning({
7419
+ rawObservationSource: observationSource,
7420
+ firstStored,
7421
+ });
7422
+ if (obsSourceWarn)
7423
+ schemaStoreWarnings.push(obsSourceWarn);
7424
+ }
7388
7425
  // #1963: surface auto-link edge retractions on the store response so a
7389
7426
  // retraction — and especially a retraction *failure* — is never silent. A
7390
7427
  // failure means a stale reference-field edge may still be live despite the
@@ -7511,7 +7548,7 @@ export async function storeStructuredForApi(params) {
7511
7548
  success: true,
7512
7549
  replayed: false,
7513
7550
  commit,
7514
- source_id: commit ? storageResult.sourceId : null,
7551
+ source_id: commit && storageResult ? storageResult.sourceId : null,
7515
7552
  ...(interpretationId
7516
7553
  ? { interpretation_id: interpretationId, interpretation_source_id: observationSourceId }
7517
7554
  : {}),
@@ -7546,14 +7583,42 @@ export async function storeStructuredForApi(params) {
7546
7583
  // target here today. An instance with enforcement: "enforced" still accepts
7547
7584
  // arbitrary raw file content through this path. See the "Known scope
7548
7585
  // boundary" section in src/services/instance_policy.ts's docblock.
7549
- async function storeUnstructuredForApi(params) {
7550
- const { fileContent, fileBuffer, filePath, mimeType, idempotencyKey, originalFilename, sourceType, userId, storageMode = "inline", } = params;
7586
+ export async function storeUnstructuredForApi(params) {
7587
+ const { fileContent, fileBuffer, filePath, mimeType, idempotencyKey, originalFilename, sourceType, userId, storageMode = "inline", commit = true, } = params;
7551
7588
  // decodeFileContent rejects non-base64 instead of letting Node discard the
7552
7589
  // invalid characters and store corrupted bytes (#2325).
7553
7590
  const resolvedFileBuffer = fileBuffer ?? (fileContent !== undefined ? decodeFileContent(fileContent) : undefined);
7554
7591
  if (!resolvedFileBuffer && storageMode !== "reference") {
7555
7592
  throw new Error("fileContent or fileBuffer is required for inline storage");
7556
7593
  }
7594
+ if (!commit) {
7595
+ // Nothing to persist in plan mode: no bytes uploaded, no `sources` row,
7596
+ // no reference row. `content_hash` is still safe (and useful) to report
7597
+ // since it's a pure function of the caller's own bytes/path, computed
7598
+ // without touching storage or the database.
7599
+ if (storageMode === "reference") {
7600
+ if (!filePath) {
7601
+ throw new Error("filePath is required for reference storage mode");
7602
+ }
7603
+ return {
7604
+ commit: false,
7605
+ source_id: null,
7606
+ storage_mode: "reference",
7607
+ reference_path: filePath,
7608
+ mime_type: mimeType,
7609
+ };
7610
+ }
7611
+ const { computeContentHash } = await import("./services/raw_storage.js");
7612
+ return {
7613
+ commit: false,
7614
+ source_id: null,
7615
+ content_hash: computeContentHash(resolvedFileBuffer),
7616
+ file_size: resolvedFileBuffer.length,
7617
+ deduplicated: false,
7618
+ entities_created: 0,
7619
+ observations_created: 0,
7620
+ };
7621
+ }
7557
7622
  if (storageMode === "reference") {
7558
7623
  if (!filePath) {
7559
7624
  throw new Error("filePath is required for reference storage mode");
@@ -7613,12 +7678,41 @@ async function handleStorePost(req, res) {
7613
7678
  });
7614
7679
  return sendStoreValidationError(res, parsed.error.issues);
7615
7680
  }
7681
+ let storeDataOrigins;
7682
+ // #2242: peel the reserved per-entity `data_origin` key off every entity and
7683
+ // resolve each entity's effective origin (entity → request → none) before
7684
+ // anything else reads the entities, so no downstream path sees it as a field.
7685
+ if (parsed.data.entities?.length) {
7686
+ const { splitEntityDataOrigins } = await import("./services/provenance_data_origin.js");
7687
+ const split = splitEntityDataOrigins(parsed.data.entities, parsed.data.data_origin);
7688
+ parsed.data.entities = split.entities;
7689
+ storeDataOrigins = split.origins;
7690
+ }
7616
7691
  try {
7617
7692
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
7693
+ const { assertConditionalStoreRequest } = await import("./services/store_condition_request.js");
7694
+ assertConditionalStoreRequest(parsed.data);
7618
7695
  const hasEntities = Boolean(parsed.data.entities?.length);
7619
7696
  const hasFileContent = Boolean(parsed.data.file_content && parsed.data.mime_type);
7620
7697
  const hasFilePath = Boolean(parsed.data.file_path);
7621
7698
  const hasUnstructured = hasFileContent || hasFilePath;
7699
+ if (hasEntities && hasUnstructured && parsed.data.commit !== false) {
7700
+ const { claimCombinedStoreKeys } = await import("./services/store_combined_admission.js");
7701
+ await claimCombinedStoreKeys({
7702
+ userId,
7703
+ entities: parsed.data.entities,
7704
+ sourcePriority: parsed.data.source_priority,
7705
+ idempotencyKey: parsed.data.idempotency_key,
7706
+ relationships: parsed.data.relationships,
7707
+ interpretation: parsed.data.interpretation,
7708
+ sourcePeerId: parsed.data.source_peer_id,
7709
+ strict: parsed.data.strict,
7710
+ }, {
7711
+ key: parsed.data.file_idempotency_key,
7712
+ content: parsed.data.file_content,
7713
+ filePath: parsed.data.file_path,
7714
+ });
7715
+ }
7622
7716
  let structuredResult;
7623
7717
  let unstructuredResult;
7624
7718
  const storeUnstructuredFromRequest = async () => {
@@ -7679,6 +7773,7 @@ async function handleStorePost(req, res) {
7679
7773
  originalFilename,
7680
7774
  sourceType: parsed.data.source_type,
7681
7775
  storageMode: parsed.data.source_storage,
7776
+ commit: parsed.data.commit,
7682
7777
  });
7683
7778
  };
7684
7779
  if (hasUnstructured && parsed.data.interpretation?.source_ref === "unstructured") {
@@ -7695,6 +7790,7 @@ async function handleStorePost(req, res) {
7695
7790
  const doStore = () => storeStructuredForApi({
7696
7791
  userId,
7697
7792
  entities: parsed.data.entities,
7793
+ expectedEntityAbsent: parsed.data.expected_entity_absent,
7698
7794
  sourcePriority: parsed.data.source_priority ?? 100,
7699
7795
  observationSource: parsed.data.observation_source,
7700
7796
  sourcePeerId: parsed.data.source_peer_id,
@@ -7709,6 +7805,7 @@ async function handleStorePost(req, res) {
7709
7805
  : undefined,
7710
7806
  commit: parsed.data.commit,
7711
7807
  strict: parsed.data.strict,
7808
+ dataOrigins: storeDataOrigins,
7712
7809
  });
7713
7810
  // A request body can only claim an external actor; verified tiers are
7714
7811
  // assigned by server-side verification paths, never taken from input.
@@ -7753,6 +7850,15 @@ async function handleStorePost(req, res) {
7753
7850
  return sendError(res, 400, error.code, error.message, error.details);
7754
7851
  }
7755
7852
  const errCode = error && typeof error === "object" ? error.code : undefined;
7853
+ const { StoreConditionError } = await import("./services/store_condition_keys.js");
7854
+ if (error instanceof StoreConditionError) {
7855
+ const status = error.code === "VALIDATION_ERROR"
7856
+ ? 400
7857
+ : error.code === "STORE_RECEIPT_UNCERTAIN" || error.code === "STORE_CONDITIONAL_FAILED"
7858
+ ? 503
7859
+ : 409;
7860
+ return res.status(status).json({ error: error.toErrorEnvelope() });
7861
+ }
7756
7862
  if (isGrantPinConflict(error)) {
7757
7863
  logWarn("AgentGrantPinConflict:store", req, { code: error.code });
7758
7864
  return sendError(res, 409, error.code, error.message, { field: error.field });
@@ -7779,6 +7885,16 @@ async function handleStorePost(req, res) {
7779
7885
  logWarn("IdempotencyCollision:store", req, { message });
7780
7886
  return sendError(res, 409, "ERR_IDEMPOTENCY_COLLISION", message);
7781
7887
  }
7888
+ if (error && typeof error === "object" && errCode === "ERR_STORE_WRITE_INCOMPLETE") {
7889
+ const message = error instanceof Error ? error.message : String(error);
7890
+ const hint = error.hint;
7891
+ logWarn("StoreWriteIncomplete:store", req, { message });
7892
+ const incompleteDetails = error.details;
7893
+ return sendError(res, 409, "ERR_STORE_WRITE_INCOMPLETE", message, {
7894
+ ...(hint ? { hint } : {}),
7895
+ ...(incompleteDetails ?? {}),
7896
+ });
7897
+ }
7782
7898
  if (error && typeof error === "object" && errCode === "ERR_STORE_POLICY_DENIED") {
7783
7899
  const err = error;
7784
7900
  // Log counts and reason codes only. The denied entities' field values are
@@ -7867,6 +7983,16 @@ async function handleStorePost(req, res) {
7867
7983
  hint: error.hint,
7868
7984
  });
7869
7985
  }
7986
+ // A capability refusal is the caller's authorization, not a server fault:
7987
+ // 403 with the same envelope `handleApiError` gives every other route.
7988
+ // Falling through made a denied edge (or entity type) read as a retryable
7989
+ // 500 DB_QUERY_FAILED.
7990
+ if (error instanceof AgentCapabilityError) {
7991
+ logWarn("AgentCapabilityRejection:store", req, error.toErrorEnvelope());
7992
+ return res
7993
+ .status(403)
7994
+ .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
7995
+ }
7870
7996
  logError("APIError:store", req, error);
7871
7997
  const message = error instanceof Error ? error.message : "Failed to store payload";
7872
7998
  return sendError(res, 500, "DB_QUERY_FAILED", message);
@@ -8318,7 +8444,7 @@ app.post("/get_entity_snapshot", async (req, res) => {
8318
8444
  });
8319
8445
  return sendValidationError(res, parsed.error.issues);
8320
8446
  }
8321
- const { entity_id, at, at_ingested } = parsed.data;
8447
+ const { entity_id, at, at_ingested, include_cleared_fields } = parsed.data;
8322
8448
  let userId;
8323
8449
  try {
8324
8450
  userId = await getAuthenticatedUserId(req);
@@ -8330,22 +8456,42 @@ app.post("/get_entity_snapshot", async (req, res) => {
8330
8456
  // helper so the offline path honours them with the same semantics as the MCP
8331
8457
  // path in server.ts. Without cutoffs, fall through to the fast materialized
8332
8458
  // entity_snapshots table read.
8333
- if (at || at_ingested) {
8459
+ if (at || at_ingested || include_cleared_fields) {
8334
8460
  try {
8335
- const result = await computeEntitySnapshotAtTime(entity_id, userId, at, at_ingested);
8461
+ const result = await computeEntitySnapshotAtTime(entity_id, userId, at, at_ingested, {
8462
+ includeClearedFields: include_cleared_fields,
8463
+ });
8464
+ if (include_cleared_fields && !at && !at_ingested) {
8465
+ const { isEntityDeleted } = await import("./services/deletion.js");
8466
+ if (result && (await isEntityDeleted(result.entity_id, userId)))
8467
+ return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
8468
+ }
8336
8469
  if (result === null) {
8337
8470
  return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
8338
8471
  }
8339
8472
  logDebug("Success:get_entity_snapshot:at_time", req, { entity_id, at, at_ingested });
8340
- return res.json(result);
8473
+ return res.json(await withSnapshotDataOrigins(result, userId));
8341
8474
  }
8342
8475
  catch (err) {
8343
8476
  logError("Error:get_entity_snapshot:at_time", req, err);
8344
8477
  return sendError(res, 500, "DB_QUERY_FAILED", err.message);
8345
8478
  }
8346
8479
  }
8347
- // Fast path: no cutoffs — read the materialized snapshot directly, scoped to
8348
- // the authenticated user.
8480
+ // Materialization is derived state. Validate owned current authority before
8481
+ // serving its row, including restrictive corruption acquisition failures.
8482
+ try {
8483
+ const { isEntityDeleted } = await import("./services/deletion.js");
8484
+ const { hasRecordedEntityLifecycleCutover } = await import("./services/entity_lifecycle_storage.js");
8485
+ const { getDb } = await import("./repositories/db/connection.js");
8486
+ if ((await hasRecordedEntityLifecycleCutover(await getDb())) &&
8487
+ (await isEntityDeleted(entity_id, userId)))
8488
+ return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
8489
+ }
8490
+ catch (err) {
8491
+ logError("Error:get_entity_snapshot:authority", req, err);
8492
+ return sendError(res, 500, "DB_QUERY_FAILED", "Entity lifecycle authority acquisition failed");
8493
+ }
8494
+ // No cutoffs: return the existing owned materialized factual snapshot.
8349
8495
  const { data, error } = await db
8350
8496
  .from("entity_snapshots")
8351
8497
  .select("*")
@@ -8360,8 +8506,23 @@ app.post("/get_entity_snapshot", async (req, res) => {
8360
8506
  return sendError(res, 500, "DB_QUERY_FAILED", error.message);
8361
8507
  }
8362
8508
  logDebug("Success:get_entity_snapshot", req, { entity_id });
8363
- return res.json(data);
8509
+ return res.json(await withSnapshotDataOrigins(data, userId));
8364
8510
  });
8511
+ /**
8512
+ * #2242: attach `data_origins` (observation_id → caller-claimed origin) for the
8513
+ * observations a snapshot's field provenance cites. The key is omitted when no
8514
+ * cited observation carries an origin, so pre-#2242 responses are unchanged.
8515
+ * Mirrors MCP `retrieve_entity_snapshot`.
8516
+ */
8517
+ async function withSnapshotDataOrigins(snapshot, userId) {
8518
+ if (!snapshot || typeof snapshot !== "object")
8519
+ return snapshot;
8520
+ const { collectSnapshotDataOrigins } = await import("./services/provenance_data_origin.js");
8521
+ const dataOrigins = await collectSnapshotDataOrigins(snapshot.provenance, userId);
8522
+ if (Object.keys(dataOrigins).length === 0)
8523
+ return snapshot;
8524
+ return { ...snapshot, data_origins: dataOrigins };
8525
+ }
8365
8526
  // List observations for entity
8366
8527
  app.post("/list_observations", async (req, res) => {
8367
8528
  const parsed = ListObservationsRequestSchema.safeParse(req.body);
@@ -8505,6 +8666,14 @@ app.post("/create_relationship", async (req, res) => {
8505
8666
  "same store call referenced by index, then link it.",
8506
8667
  });
8507
8668
  }
8669
+ // Relationship-type + endpoint-type capability refusal, raised inside
8670
+ // relationshipsService.createRelationship (the shared edge-write gate).
8671
+ if (error instanceof AgentCapabilityError) {
8672
+ logWarn("AgentCapabilityRejection:create_relationship", req, error.toErrorEnvelope());
8673
+ return res
8674
+ .status(403)
8675
+ .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
8676
+ }
8508
8677
  logError("RelationshipCreationError:create_relationship", req, error);
8509
8678
  return sendError(res, 500, "DB_QUERY_FAILED", error instanceof Error ? error.message : "Failed to create relationship");
8510
8679
  }
@@ -8540,11 +8709,19 @@ app.post("/create_relationships", async (req, res) => {
8540
8709
  });
8541
8710
  }
8542
8711
  catch (error) {
8543
- errors.push({
8712
+ const item = {
8544
8713
  index,
8545
8714
  relationship,
8546
8715
  error: error instanceof Error ? error.message : String(error),
8547
- });
8716
+ };
8717
+ // Batch semantics stay HTTP 200 so successful siblings are retained,
8718
+ // but a capability denial must keep the same machine-actionable repair
8719
+ // contract as the single-item endpoint. Agents rely on code/op/hint;
8720
+ // reducing this to error.message turns a recoverable refusal opaque.
8721
+ if (error instanceof AgentCapabilityError) {
8722
+ Object.assign(item, error.toErrorEnvelope());
8723
+ }
8724
+ errors.push(item);
8548
8725
  }
8549
8726
  }
8550
8727
  logDebug("Success:create_relationships", req, {
@@ -9267,9 +9444,13 @@ const handleIssuesSyncHttp = async (req, res) => {
9267
9444
  since: parsed.data.since,
9268
9445
  state: parsed.data.state,
9269
9446
  labels: parsed.data.labels,
9447
+ repo: parsed.data.repo,
9270
9448
  push: parsed.data.push,
9449
+ commit: parsed.data.commit,
9271
9450
  });
9272
9451
  logDebug("Success:issues_sync", req, {
9452
+ repo: result.repo,
9453
+ dry_run: result.dry_run,
9273
9454
  issues_synced: result.issues_synced,
9274
9455
  messages_synced: result.messages_synced,
9275
9456
  issues_pushed: result.issues_pushed,
@@ -9346,6 +9527,15 @@ app.post("/delete_relationship", async (req, res) => {
9346
9527
  "one of the returned types.",
9347
9528
  });
9348
9529
  }
9530
+ // Tombstoning a live edge changes the same relationship state that the
9531
+ // create/restore capability governs. Enforce the admitted grant after the
9532
+ // discovery guard (so a missing edge stays not-found) and before writing
9533
+ // the deletion observation.
9534
+ const { enforceCurrentAgentRelationshipWrites } = await import("./services/relationship_write_capability.js");
9535
+ await enforceCurrentAgentRelationshipWrites({
9536
+ userId,
9537
+ relationships: [{ relationship_type, source_entity_id, target_entity_id }],
9538
+ });
9349
9539
  const { softDeleteRelationship } = await import("./services/deletion.js");
9350
9540
  // Construct relationship key
9351
9541
  const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
@@ -9554,20 +9744,27 @@ app.post("/update_schema_incremental", async (req, res) => {
9554
9744
  }
9555
9745
  try {
9556
9746
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
9557
- const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
9747
+ const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version,
9748
+ // #2374: no local `= false` default. `user_specific` stays
9749
+ // `undefined` when the caller omitted it, so `updateSchemaIncremental`
9750
+ // can defer to whichever scope its own read resolved instead of being
9751
+ // told "global" by a default this route manufactured.
9752
+ user_specific, activate = true, migrate_existing = false, force = false, } = parsed.data;
9558
9753
  const { schemaRegistry, loadCodeDefinedSchemaEntry } = await import("./services/schema_registry.js");
9559
- // #2454 REST parity with MCP `updateSchemaIncremental`: the existence guard
9560
- // is scope-limited (`user_specific ? userId : undefined`). When that lookup
9561
- // misses but a schema is active in another scope, return
9562
- // ERR_SCHEMA_SCOPE_MISMATCH — never ERR_NO_SCHEMA_FOR_ENTITY_TYPE with a
9563
- // register_schema hint. Write-scope rematch itself remains #2446.
9564
- const registeredSchema = await schemaRegistry.loadActiveSchema(entity_type, user_specific ? userId : undefined);
9754
+ // #2454 / #2374 REST parity with the MCP handler: the existence guard
9755
+ // resolves the SAME row `updateSchemaIncremental` will read —
9756
+ // user-scope-preferred when `user_specific` is omitted or true,
9757
+ // global-only when the caller explicitly passed `user_specific: false`.
9758
+ // When the global-only lookup misses but a schema is active in the
9759
+ // caller's user scope, return ERR_SCHEMA_SCOPE_MISMATCH — never
9760
+ // ERR_NO_SCHEMA_FOR_ENTITY_TYPE with a register_schema hint.
9761
+ const registeredSchema = await schemaRegistry.loadActiveSchema(entity_type, user_specific === false ? undefined : userId);
9565
9762
  if (!registeredSchema) {
9566
9763
  const codeDefinedSchema = await loadCodeDefinedSchemaEntry(entity_type);
9567
9764
  if (!codeDefinedSchema) {
9568
9765
  const anyScopeSchema = await schemaRegistry.loadActiveSchema(entity_type, userId);
9569
9766
  if (anyScopeSchema) {
9570
- const guardScope = user_specific ? "user" : "global";
9767
+ const guardScope = user_specific === false ? "global" : "user";
9571
9768
  const foundScope = anyScopeSchema.scope ?? "global";
9572
9769
  return res.status(200).json({
9573
9770
  error: {
@@ -9577,13 +9774,13 @@ app.post("/update_schema_incremental", async (req, res) => {
9577
9774
  "and found none there.",
9578
9775
  hint: `The existing SchemaDefinition for "${entity_type}" lives in ` +
9579
9776
  `"${foundScope}" scope. This call did not check that scope because ` +
9580
- `user_specific was ${user_specific ? "true" : "not set (defaults to false)"}. ` +
9777
+ `user_specific was ${user_specific === false ? "false" : "true"}. ` +
9581
9778
  "This entity_type already has an active schema — creating a second one for it " +
9582
9779
  "risks leaving two active schema_registry rows for the same entity_type (see " +
9583
9780
  "#2374/#2378), after which the next incremental update can merge onto stale " +
9584
- "state and drop fields. Instead, retry update_schema_incremental with " +
9585
- `user_specific: ${foundScope === "user"} so the call resolves the scope the ` +
9586
- "schema actually lives in.",
9781
+ "state and drop fields. Instead, retry update_schema_incremental without " +
9782
+ "user_specific (the call then writes to whichever scope your schema resolves " +
9783
+ `to), or with user_specific: ${foundScope === "user"}.`,
9587
9784
  details: {
9588
9785
  entity_type,
9589
9786
  guard_scope: guardScope,
@@ -9623,9 +9820,20 @@ app.post("/update_schema_incremental", async (req, res) => {
9623
9820
  canonical_name_fields,
9624
9821
  schema_version,
9625
9822
  user_specific,
9823
+ // #2374: pass userId unconditionally — same reasoning as the MCP
9824
+ // handler. The service resolves scope from its own read, not from
9825
+ // whether this route pre-gated userId on user_specific.
9626
9826
  user_id: userId,
9627
9827
  activate,
9628
9828
  migrate_existing,
9829
+ // migrate_existing must scope to the authenticated caller so
9830
+ // raw_fragments stored by this user are promoted, matching the MCP
9831
+ // handler's migrate_user_id wiring (#2379 note: REST previously
9832
+ // relied solely on the service's `migrate_user_id ?? user_id`
9833
+ // fallback, which happened to cover this route, but stating it
9834
+ // explicitly here keeps the two surfaces in parity rather than
9835
+ // coincidentally agreeing).
9836
+ migrate_user_id: migrate_existing ? userId : undefined,
9629
9837
  force,
9630
9838
  });
9631
9839
  }
@@ -9636,21 +9844,38 @@ app.post("/update_schema_incremental", async (req, res) => {
9636
9844
  }
9637
9845
  throw err;
9638
9846
  }
9847
+ // #2379: derive migrated_existing from the ACTUAL result, never from
9848
+ // the `migrate_existing` request flag.
9849
+ const migrationResult = newSchema.migration_result;
9850
+ // #2374: report the scope actually written, read off the persisted row.
9851
+ const scopeWritten = newSchema.scope === "user" ? "user" : "global";
9639
9852
  logDebug("Success:update_schema_incremental", req, {
9640
9853
  entity_type,
9641
9854
  fields_added: (fields_to_add || []).length,
9642
9855
  fields_removed: (fields_to_remove || []).length,
9643
9856
  migrate_existing,
9857
+ migrated_existing: (migrationResult?.migrated_count ?? 0) > 0,
9858
+ scope: scopeWritten,
9644
9859
  });
9645
9860
  return res.json({
9646
9861
  success: true,
9647
9862
  schema: newSchema,
9648
9863
  schema_version: newSchema.schema_version,
9864
+ fields_added: (fields_to_add || []).map((f) => f.field_name),
9649
9865
  fields_removed: fields_to_remove || [],
9650
9866
  // Echo the resolved identity rule so CLI/HTTP callers can confirm the
9651
9867
  // re-key without a second describe_entity_type round trip (#2020 ux).
9652
9868
  canonical_name_fields: newSchema.schema_definition
9653
9869
  .canonical_name_fields ?? null,
9870
+ activated: activate,
9871
+ // #2379: true only when the migration actually promoted something.
9872
+ migrated_existing: (migrationResult?.migrated_count ?? 0) > 0,
9873
+ // Present only when migrate_existing was requested this call.
9874
+ ...(migrationResult ? { migration_result: migrationResult } : {}),
9875
+ // #2374: the scope this call actually wrote to, read off the
9876
+ // persisted row rather than re-derived from the request.
9877
+ scope: scopeWritten,
9878
+ ...(scopeWritten === "user" ? { user_id: newSchema.user_id ?? null } : {}),
9654
9879
  });
9655
9880
  }
9656
9881
  catch (error) {
@@ -9871,6 +10096,27 @@ app.post("/register_schema", async (req, res) => {
9871
10096
  }
9872
10097
  });
9873
10098
  // POST /correct - Create correction observation to override field value
10099
+ app.post("/corrections/transaction", async (req, res) => {
10100
+ try {
10101
+ const userId = await getAuthenticatedUserId(req, req.body?.user_id);
10102
+ const { applyCorrectionTransaction, CorrectionTransactionError } = await import("./services/correction_transaction.js");
10103
+ try {
10104
+ return res.json(await applyCorrectionTransaction({ ...req.body, user_id: userId }));
10105
+ }
10106
+ catch (error) {
10107
+ if (error instanceof CorrectionTransactionError) {
10108
+ const status = error.code === "VALIDATION_ERROR" ? 400 : error.code === "RESOURCE_NOT_FOUND" ? 404 : 409;
10109
+ return sendError(res, status, error.code, error.message, {
10110
+ hint: "Read current entity snapshots and resolve conflicts; retry an uncertain request with exactly the original payload and key.",
10111
+ });
10112
+ }
10113
+ throw error;
10114
+ }
10115
+ }
10116
+ catch (error) {
10117
+ return handleApiError(req, res, error, "Correction transaction failed and was rolled back.");
10118
+ }
10119
+ });
9874
10120
  app.post("/correct", async (req, res) => {
9875
10121
  const parsed = CorrectEntityRequestSchema.safeParse(req.body);
9876
10122
  if (!parsed.success) {
@@ -9881,7 +10127,7 @@ app.post("/correct", async (req, res) => {
9881
10127
  }
9882
10128
  try {
9883
10129
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
9884
- const { entity_id, entity_type, field, value, idempotency_key } = parsed.data;
10130
+ const { entity_id, entity_type, field, value, idempotency_key, expected_version, overwrite } = parsed.data;
9885
10131
  const correctCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
9886
10132
  if (correctCtx) {
9887
10133
  enforceAgentCapability("correct", [entity_type], correctCtx);
@@ -9898,9 +10144,9 @@ app.post("/correct", async (req, res) => {
9898
10144
  // propagates (it is NOT coerced to "declared field" — product principle
9899
10145
  // 10.2); a missing schema throws CorrectionSchemaNotFoundError, surfaced as
9900
10146
  // ERR_NO_SCHEMA_FOR_ENTITY_TYPE. Both reach the catch below.
9901
- const { createCorrection, resolveCorrectionSchema, buildCorrectionResponse } = await import("./services/correction.js");
10147
+ const { createCorrection, createCorrectionWithVersionPrecondition, resolveCorrectionSchema, buildCorrectionResponse, } = await import("./services/correction.js");
9902
10148
  const { schemaVersion: correctionSchemaVersion, isUnknownField } = await resolveCorrectionSchema(entity_type, field, userId);
9903
- const result = await createCorrection({
10149
+ const correctionParams = {
9904
10150
  entity_id,
9905
10151
  entity_type,
9906
10152
  field,
@@ -9908,7 +10154,14 @@ app.post("/correct", async (req, res) => {
9908
10154
  schema_version: correctionSchemaVersion,
9909
10155
  user_id: userId,
9910
10156
  idempotency_key,
9911
- });
10157
+ };
10158
+ const result = typeof expected_version === "string"
10159
+ ? await createCorrectionWithVersionPrecondition({
10160
+ ...correctionParams,
10161
+ expected_version,
10162
+ overwrite,
10163
+ })
10164
+ : await createCorrection(correctionParams);
9912
10165
  if (isUnknownField) {
9913
10166
  try {
9914
10167
  const { storeFragment } = await import("./services/raw_fragments.js");
@@ -9939,8 +10192,10 @@ app.post("/correct", async (req, res) => {
9939
10192
  entity_id,
9940
10193
  entity_type,
9941
10194
  field,
9942
- value,
10195
+ value: result.value,
9943
10196
  isUnknownField,
10197
+ replayed: result.replayed,
10198
+ entity_version: result.entity_version,
9944
10199
  }),
9945
10200
  });
9946
10201
  }
@@ -9949,13 +10204,43 @@ app.post("/correct", async (req, res) => {
9949
10204
  // (ERR_NO_SCHEMA_FOR_ENTITY_TYPE), not an internal failure. A schema-registry
9950
10205
  // IO failure is NOT swallowed here — it falls through to the generic 500
9951
10206
  // handler below instead of being coerced into a "declared field" outcome.
9952
- const { CorrectionSchemaNotFoundError } = await import("./services/correction.js");
10207
+ const { CorrectionSchemaNotFoundError, CorrectionEntityTypeMismatchError, CorrectionIdempotencyMismatchError, FieldVersionConflictError, } = await import("./services/correction.js");
9953
10208
  if (error instanceof CorrectionSchemaNotFoundError) {
9954
10209
  logWarn("ValidationError:correct", req, { code: error.code });
9955
10210
  return res
9956
10211
  .status(400)
9957
10212
  .json(buildErrorEnvelope(error.code, error.message, { entity_type: error.entityType }));
9958
10213
  }
10214
+ if (error instanceof CorrectionEntityTypeMismatchError) {
10215
+ logWarn("ValidationError:correct", req, { code: error.code });
10216
+ return res.status(400).json(buildErrorEnvelope(error.code, error.message, {
10217
+ entity_id: error.entityId,
10218
+ supplied_entity_type: error.suppliedEntityType,
10219
+ stored_entity_type: error.storedEntityType,
10220
+ }));
10221
+ }
10222
+ if (error instanceof CorrectionIdempotencyMismatchError) {
10223
+ return res.status(400).json(buildErrorEnvelope(error.code, error.message, {
10224
+ idempotency_key: error.idempotencyKey,
10225
+ hint: "Use a new idempotency_key for a distinct correction payload.",
10226
+ }));
10227
+ }
10228
+ // Waxwing ADR: stale expected_version on /correct. Nothing was written —
10229
+ // the precondition check runs before createCorrection. Flat standard
10230
+ // envelope (error_code/message/details), matching the OpenAPI contract
10231
+ // and the sibling ERR_ARRAY_ITEM_CONFLICT 409 on /patch_array_item — NOT
10232
+ // wrapped under an `error` key (that shape is reserved for envelopes
10233
+ // like entity_owner_conflict that predate the standard envelope).
10234
+ if (error instanceof FieldVersionConflictError) {
10235
+ logWarn("VersionConflict:correct", req, { code: error.code, field: error.field });
10236
+ return res.status(409).json(buildErrorEnvelope(error.code, error.message, {
10237
+ entity_id: error.entityId,
10238
+ field: error.field,
10239
+ stored_version: error.storedVersion,
10240
+ expected_version: error.expectedVersion,
10241
+ hint: "Re-read the entity snapshot and retry with its current entity_version as expected_version.",
10242
+ }));
10243
+ }
9959
10244
  // Instance store-policy denial (#1975) — same envelope as /store, so a
9960
10245
  // caller handling one handles both.
9961
10246
  const { StorePolicyDeniedError } = await import("./services/instance_policy.js");
@@ -9969,6 +10254,131 @@ app.post("/correct", async (req, res) => {
9969
10254
  return handleApiError(req, res, error, "Failed to create correction", "DB_QUERY_FAILED", "APIError:correct");
9970
10255
  }
9971
10256
  });
10257
+ // POST /patch_array_item - Atomically patch one item of a structured array
10258
+ // field by key (Waxwing ADR, ent_4b41bb83a4faf4428a73bfc8). Sibling of
10259
+ // /correct; see openapi.yaml for the full contract.
10260
+ app.post("/patch_array_item", writeRateLimit, async (req, res) => {
10261
+ const parsed = PatchArrayItemRequestSchema.safeParse(req.body);
10262
+ if (!parsed.success) {
10263
+ logWarn("ValidationError:patch_array_item", req, { issues: parsed.error.issues });
10264
+ return sendValidationError(res, parsed.error.issues);
10265
+ }
10266
+ try {
10267
+ const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
10268
+ const { entity_id, entity_type, field, key_field, key_value, item, expected_item_version, expected_item_absent, } = parsed.data;
10269
+ // Resolve the authoritative entity type under the authenticated user
10270
+ // before capability/protected-type checks. The request's entity_type is a
10271
+ // consistency assertion, never an authorization input.
10272
+ const { loadArrayItemPatchTarget } = await import("./services/array_item_patch.js");
10273
+ const target = await loadArrayItemPatchTarget({
10274
+ entityId: entity_id,
10275
+ userId,
10276
+ suppliedEntityType: entity_type,
10277
+ });
10278
+ const correctCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
10279
+ if (correctCtx) {
10280
+ enforceAgentCapability("correct", [target.entity_type], correctCtx);
10281
+ }
10282
+ assertCanWriteProtectedBatch({
10283
+ entity_types: [target.entity_type],
10284
+ op: "correct",
10285
+ identity: getCurrentAgentIdentity(),
10286
+ admission: getCurrentAAuthAdmission(),
10287
+ });
10288
+ const { patchArrayItem } = await import("./services/array_item_patch.js");
10289
+ const result = await patchArrayItem({
10290
+ entity_id,
10291
+ entity_type,
10292
+ user_id: userId,
10293
+ field,
10294
+ key_field,
10295
+ key_value,
10296
+ item,
10297
+ expected_item_version,
10298
+ expected_item_absent,
10299
+ idempotency_key: parsed.data.idempotency_key,
10300
+ });
10301
+ if (result.status === "conflict") {
10302
+ logWarn("VersionConflict:patch_array_item", req, { entity_id, field, key_field });
10303
+ return res.status(409).json(buildErrorEnvelope("ERR_ARRAY_ITEM_CONFLICT", `Stale expected_item_version for ${entity_type}.${field}[${key_field}=${JSON.stringify(key_value)}]`, {
10304
+ entity_id,
10305
+ entity_type,
10306
+ field,
10307
+ key_field,
10308
+ key_value,
10309
+ current_item: result.conflict?.current_item ?? null,
10310
+ current_item_version: result.conflict?.current_item_version ?? null,
10311
+ expected_item_version: result.conflict?.expected_item_version ?? null,
10312
+ expected_item_absent: expected_item_absent ?? false,
10313
+ hint: "Re-apply the intended change to current_item and retry with current_item_version as expected_item_version.",
10314
+ }));
10315
+ }
10316
+ return res.json({
10317
+ success: true,
10318
+ entity_id: result.entity_id,
10319
+ entity_type: result.entity_type,
10320
+ field: result.field,
10321
+ key_field: result.key_field,
10322
+ key_value: result.key_value,
10323
+ observation_id: result.observation_id,
10324
+ item: result.item,
10325
+ item_version: result.item_version,
10326
+ array_length: result.array_length,
10327
+ snapshot: result.snapshot,
10328
+ replayed: result.replayed ?? false,
10329
+ });
10330
+ }
10331
+ catch (error) {
10332
+ if (error instanceof Error && error.message.startsWith("Entity not found")) {
10333
+ return sendError(res, 404, "RESOURCE_NOT_FOUND", error.message);
10334
+ }
10335
+ const { CorrectionIdempotencyMismatchError } = await import("./services/correction.js");
10336
+ if (error instanceof CorrectionIdempotencyMismatchError) {
10337
+ return res.status(400).json(buildErrorEnvelope(error.code, error.message, {
10338
+ idempotency_key: error.idempotencyKey,
10339
+ hint: "Use a new idempotency_key for a distinct patch payload.",
10340
+ }));
10341
+ }
10342
+ if (error instanceof EntityOwnerConflictError) {
10343
+ return res.status(409).json({ error: error.toErrorEnvelope() });
10344
+ }
10345
+ {
10346
+ const { ArrayItemKeyFieldMismatchError, ArrayItemPolicyRequiredError, ArrayItemEntityTypeMismatchError, } = await import("./services/array_item_patch.js");
10347
+ if (error instanceof ArrayItemKeyFieldMismatchError ||
10348
+ error instanceof ArrayItemPolicyRequiredError ||
10349
+ error instanceof ArrayItemEntityTypeMismatchError) {
10350
+ logWarn("KeyFieldMismatch:patch_array_item", req, {
10351
+ code: error.code,
10352
+ });
10353
+ return res
10354
+ .status(error.statusCode)
10355
+ .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
10356
+ }
10357
+ }
10358
+ // Instance store-policy denial (#1975) — patchArrayItem() writes through
10359
+ // createCorrection exactly like /correct, so it can be refused by the
10360
+ // same instance-level write policy. Same envelope shape as /correct and
10361
+ // /store, so a caller handling one handles all three.
10362
+ const { StorePolicyDeniedError } = await import("./services/instance_policy.js");
10363
+ if (error instanceof StorePolicyDeniedError) {
10364
+ logWarn("StorePolicyDenied:patch_array_item", req, {
10365
+ denied_count: error.denied.length,
10366
+ reason_codes: [...new Set(error.denied.map((d) => d.reason_code))].sort(),
10367
+ });
10368
+ return res.status(400).json({ error: error.toErrorEnvelope() });
10369
+ }
10370
+ if (error instanceof StorePolicyUnavailableError) {
10371
+ logError("StorePolicyUnavailable:patch_array_item", req, error, {
10372
+ code: error.code,
10373
+ cause: error.cause_message,
10374
+ });
10375
+ return res
10376
+ .status(503)
10377
+ .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
10378
+ }
10379
+ return handleApiError(req, res, error, "Failed to patch array item", "DB_QUERY_FAILED", "APIError:patch_array_item");
10380
+ }
10381
+ });
9972
10382
  // POST /get_authenticated_user - Get authenticated user ID
9973
10383
  // FU-2026-05-003: GET /conversations/:conversation_id/turn-index — Inspector
9974
10384
  // per-turn anchor sections and turn timeline sidebar.
@@ -10784,14 +11194,12 @@ function assertRouteTableIsReachable() {
10784
11194
  * loopback while the socket was actually open on all interfaces).
10785
11195
  */
10786
11196
  export function resolveHttpBindHost(env = process.env) {
10787
- const raw = (env.NEOTOMA_HTTP_HOST || "").trim();
10788
- return raw.length > 0 ? raw : "127.0.0.1";
10789
- }
10790
- /** True when the given bind host resolves to a loopback-only address. */
10791
- export function isLoopbackHost(host) {
10792
- const normalized = host.trim().toLowerCase();
10793
- return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
11197
+ return resolveBindHostFromEnv("NEOTOMA_HTTP_HOST", env);
10794
11198
  }
11199
+ // Re-exported so existing importers of `isLoopbackHost` from this module keep
11200
+ // working; the implementation lives in `./shared/bind_host.ts`, which the
11201
+ // standalone MCP WebSocket bridge also uses.
11202
+ export { isLoopbackHost };
10795
11203
  /**
10796
11204
  * Try to bind on a port/host; resolves with server and port, or rejects on
10797
11205
  * error (e.g. EADDRINUSE).