neotoma 0.25.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 (433) hide show
  1. package/README.md +77 -47
  2. package/dist/actions.d.ts +116 -27
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +560 -191
  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 +993 -220
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/parse_schema_fields.d.ts +2 -1
  14. package/dist/cli/parse_schema_fields.d.ts.map +1 -1
  15. package/dist/cli/parse_schema_fields.js +4 -1
  16. package/dist/cli/parse_schema_fields.js.map +1 -1
  17. package/dist/docs/api/rest_api.md +169 -365
  18. package/dist/docs/architecture/architecture.md +250 -830
  19. package/dist/docs/architecture/idempotence_pattern.md +101 -0
  20. package/dist/docs/conventions/documentation_standards.md +184 -75
  21. package/dist/docs/developer/agentic_eval_fixture_format.md +2 -0
  22. package/dist/docs/developer/atomic_corrections.md +17 -0
  23. package/dist/docs/developer/cli_agent_instructions.md +30 -4
  24. package/dist/docs/developer/cli_reference.md +217 -170
  25. package/dist/docs/developer/fleet_onboarding.md +7 -6
  26. package/dist/docs/developer/mcp/instructions.md +29 -18
  27. package/dist/docs/developer/mcp_overview.md +4 -4
  28. package/dist/docs/integrations/matrix.md +4 -4
  29. package/dist/docs/operations/configuration.md +2 -1
  30. package/dist/docs/operations/running_the_server.md +1 -1
  31. package/dist/docs/reference/error_codes.md +1 -0
  32. package/dist/docs/specs/MCP_SPEC.md +5 -4
  33. package/dist/docs/subsystems/agent_attribution_integration.md +159 -0
  34. package/dist/docs/subsystems/agent_capabilities.md +57 -1
  35. package/dist/docs/subsystems/agentic_eval.md +13 -2
  36. package/dist/docs/subsystems/deletion.md +24 -9
  37. package/dist/docs/subsystems/errors.md +80 -45
  38. package/dist/docs/subsystems/ingestion/ingestion.md +1 -1
  39. package/dist/docs/subsystems/reducer.md +208 -52
  40. package/dist/docs/subsystems/schema_registry.md +4 -1
  41. package/dist/docs/subsystems/sources.md +25 -0
  42. package/dist/docs/subsystems/substrate_events.md +1 -1
  43. package/dist/docs/testing/automated_test_catalog.md +89 -20
  44. package/dist/docs/testing/testing_standard.md +33 -0
  45. package/dist/docs/vocabulary/canonical_terms.md +212 -180
  46. package/dist/embeddings.d.ts +4 -1
  47. package/dist/embeddings.d.ts.map +1 -1
  48. package/dist/embeddings.js +7 -10
  49. package/dist/embeddings.js.map +1 -1
  50. package/dist/inspector/assets/{access_policies-B1HM9aJQ.js → access_policies-CJOlV60d.js} +1 -1
  51. package/dist/inspector/assets/{agent_badge-XmciduFJ.js → agent_badge-BnBqn8kt.js} +1 -1
  52. package/dist/inspector/assets/{agent_detail-DOEe2mhV.js → agent_detail-wP0ccjhX.js} +1 -1
  53. package/dist/inspector/assets/{agent_filter-Dio0XybG.js → agent_filter-Bp2xA131.js} +1 -1
  54. package/dist/inspector/assets/{agent_grant_detail-BfUFnclY.js → agent_grant_detail-HIGr5dq_.js} +1 -1
  55. package/dist/inspector/assets/agent_grant_form-BoNBvzEf.js +1 -0
  56. package/dist/inspector/assets/{agent_grants-DZaHIPk8.js → agent_grants-CTKkadE5.js} +1 -1
  57. package/dist/inspector/assets/{agents-B-f-NRE2.js → agents-CPwJLNJN.js} +1 -1
  58. package/dist/inspector/assets/{api_not_configured_state-Ou-G3uKz.js → api_not_configured_state-ivqIcs2c.js} +1 -1
  59. package/dist/inspector/assets/{attribution_card-CIkjiijF.js → attribution_card-5ZAqRz6K.js} +1 -1
  60. package/dist/inspector/assets/{attribution_summary-0ckzYad9.js → attribution_summary-D_MHFv7e.js} +1 -1
  61. package/dist/inspector/assets/{bundles-Burq1ci7.js → bundles-QfMciBj9.js} +1 -1
  62. package/dist/inspector/assets/{card-CTv_O7eJ.js → card-1rkzdi9W.js} +1 -1
  63. package/dist/inspector/assets/{checkbox--jk4IAjc.js → checkbox-CO5dXSZf.js} +1 -1
  64. package/dist/inspector/assets/{compliance-B3vrfBCU.js → compliance-OTGEtY7Y.js} +1 -1
  65. package/dist/inspector/assets/{confirm-dialog-DgDSsLkR.js → confirm-dialog-D0cZ5vck.js} +1 -1
  66. package/dist/inspector/assets/{conversation_common-BAdG50H9.js → conversation_common-cSILaHXq.js} +1 -1
  67. package/dist/inspector/assets/{conversation_detail-DIUc4HKE.js → conversation_detail-DT-lGapN.js} +1 -1
  68. package/dist/inspector/assets/{copy_id_button-D1VkPBFT.js → copy_id_button-Dz0JDn4l.js} +1 -1
  69. package/dist/inspector/assets/{copyable_code_block-ChPCJ02V.js → copyable_code_block-B2f99g8Q.js} +1 -1
  70. package/dist/inspector/assets/{corrections-ChJ2p7_i.js → corrections-KvxGvvLV.js} +1 -1
  71. package/dist/inspector/assets/{dashboard-DIYNcYg6.js → dashboard-BudvW21X.js} +1 -1
  72. package/dist/inspector/assets/{data-table-C4VXfuGY.js → data-table-d9EMxowf.js} +1 -1
  73. package/dist/inspector/assets/{design-DXOwL75T.js → design-DyiQbEeP.js} +1 -1
  74. package/dist/inspector/assets/{dialog-CXVzcRia.js → dialog-BmW1Iw8o.js} +1 -1
  75. package/dist/inspector/assets/{docs-B26tYp-6.js → docs-CT3IYX0E.js} +1 -1
  76. package/dist/inspector/assets/{docs_page_layout-CrM6Un_f.js → docs_page_layout-CosNjUZa.js} +1 -1
  77. package/dist/inspector/assets/{embed_graph-C2Jc2iwk.js → embed_graph-Dk1-zVnz.js} +1 -1
  78. package/dist/inspector/assets/{empty_state-d5guw2_7.js → empty_state-B8Ot2oGB.js} +1 -1
  79. package/dist/inspector/assets/{entities-BrnaqwF9.js → entities-Dt5Ts5UN.js} +1 -1
  80. package/dist/inspector/assets/{entity_correct-DnD1oYUl.js → entity_correct-D2PHEpGY.js} +1 -1
  81. package/dist/inspector/assets/{entity_history-PFlm7n3S.js → entity_history-DHgNythA.js} +1 -1
  82. package/dist/inspector/assets/{entity_history_sections-C5gKEqH8.js → entity_history_sections-Cnxblw-Z.js} +1 -1
  83. package/dist/inspector/assets/{entity_link-BlAO6pqg.js → entity_link-DWf1VD1n.js} +1 -1
  84. package/dist/inspector/assets/{entity_relationships_by_type-DzLwN95P.js → entity_relationships_by_type-B0-TLC5o.js} +1 -1
  85. package/dist/inspector/assets/{entity_segment_page-CGm6UKGn.js → entity_segment_page-CzZ0VhKm.js} +1 -1
  86. package/dist/inspector/assets/{entity_table_column_toggle-CotVPBog.js → entity_table_column_toggle-C73aErGE.js} +1 -1
  87. package/dist/inspector/assets/{entity_timeline-DYqizEmo.js → entity_timeline-SAq5qvB5.js} +1 -1
  88. package/dist/inspector/assets/{entity_types-CntHwuK5.js → entity_types-DEcDvyO0.js} +1 -1
  89. package/dist/inspector/assets/{faq-_NXFNTgM.js → faq-DazWIFdv.js} +1 -1
  90. package/dist/inspector/assets/{graph_auto_fit-BeH9MOss.js → graph_auto_fit-CSzD7yNh.js} +1 -1
  91. package/dist/inspector/assets/{graph_explorer-CxWZohQv.js → graph_explorer-BEXXiMQ1.js} +1 -1
  92. package/dist/inspector/assets/{home-DudMw6_Z.js → home-3AZtmjKp.js} +1 -1
  93. package/dist/inspector/assets/{index-DDgXel2r.js → index-Cii0RALL.js} +1 -1
  94. package/dist/inspector/assets/{index-VT5Dxs36.js → index-Dt_q_A1j.js} +1 -1
  95. package/dist/inspector/assets/{index-BMURChxZ.js → index-fc4sMprn.js} +2 -2
  96. package/dist/inspector/assets/{index-WhUlQzmL.css → index-yoY5L3eB.css} +1 -1
  97. package/dist/inspector/assets/interpretations-CjR2csNL.js +1 -0
  98. package/dist/inspector/assets/{interpretations-xJnQ-uYX.js → interpretations-DtzoiGuK.js} +1 -1
  99. package/dist/inspector/assets/{issue_author_attribution-DOn6PLKU.js → issue_author_attribution-DtmFaluD.js} +1 -1
  100. package/dist/inspector/assets/{issue_detail-DztHpggK.js → issue_detail-ChHiz9I_.js} +1 -1
  101. package/dist/inspector/assets/{issues-BkgvGReD.js → issues-BoxR2nB2.js} +1 -1
  102. package/dist/inspector/assets/{json_viewer-xKCfoCA1.js → json_viewer-KAFp_j-8.js} +1 -1
  103. package/dist/inspector/assets/{label-DDkYZSlU.js → label-Dn0ARjYl.js} +1 -1
  104. package/dist/inspector/assets/{live_relative_time-D-U5_U5-.js → live_relative_time-gOIFOOLR.js} +1 -1
  105. package/dist/inspector/assets/{mobile_filter_popover-DQNcOXkp.js → mobile_filter_popover-2ezU-mlZ.js} +1 -1
  106. package/dist/inspector/assets/{not_found-D6QKUWRM.js → not_found-DwX7ikQC.js} +1 -1
  107. package/dist/inspector/assets/{oauth_callback-BVxx0KXw.js → oauth_callback-BDGLOyTN.js} +1 -1
  108. package/dist/inspector/assets/{oauth_signin-DI4E46_1.js → oauth_signin-CGcCtq-V.js} +1 -1
  109. package/dist/inspector/assets/{observations-C1wQYMZV.js → observations-DvcUGpHm.js} +1 -1
  110. package/dist/inspector/assets/{page_shell-EP5xmaGY.js → page_shell-DiwchdUC.js} +1 -1
  111. package/dist/inspector/assets/{pagination-B_G7Mbyi.js → pagination-BGIUHQkm.js} +1 -1
  112. package/dist/inspector/assets/{peer_detail-BC-DSDif.js → peer_detail-DeD1sTeJ.js} +1 -1
  113. package/dist/inspector/assets/{peers-fVyfV57y.js → peers-CXe47m55.js} +1 -1
  114. package/dist/inspector/assets/{pin_primitive_button-_TIZX9AI.js → pin_primitive_button-C37Ha0Bt.js} +1 -1
  115. package/dist/inspector/assets/{recent_activity-CFv4LsTR.js → recent_activity-CzQjt4ym.js} +1 -1
  116. package/dist/inspector/assets/{recent_conversations-DU1XrX7M.js → recent_conversations-BFYZTxUJ.js} +1 -1
  117. package/dist/inspector/assets/{recent_conversations-Cac7CkDg.js → recent_conversations-BnUl4h4t.js} +1 -1
  118. package/dist/inspector/assets/{recent_records_feed-BFJzHH4J.js → recent_records_feed-y1n0RuMa.js} +1 -1
  119. package/dist/inspector/assets/record_activity-kXm-jtQ6.js +1 -0
  120. package/dist/inspector/assets/{relationship_detail-Cc9dVYco.js → relationship_detail-ic83ZCwG.js} +1 -1
  121. package/dist/inspector/assets/{relationship_panel_groups-Bknw8oKA.js → relationship_panel_groups-CI1fZIU_.js} +1 -1
  122. package/dist/inspector/assets/{relationships-DoCpO8F8.js → relationships-C1JrEl85.js} +1 -1
  123. package/dist/inspector/assets/{relationships-CJ7GneMs.js → relationships-C7iSdgXG.js} +1 -1
  124. package/dist/inspector/assets/{sandbox-BrYkvSj5.js → sandbox-Dst-WJhB.js} +1 -1
  125. package/dist/inspector/assets/{schema_detail-CdX2n82a.js → schema_detail-C7_JSWAw.js} +1 -1
  126. package/dist/inspector/assets/{schemas-CR6vDZ-d.js → schemas-BgrIpRVJ.js} +1 -1
  127. package/dist/inspector/assets/{search-uJmTOqpx.js → search-BUmT32dy.js} +1 -1
  128. package/dist/inspector/assets/{segmented_control-Dgdqohml.js → segmented_control-t1pBImFE.js} +1 -1
  129. package/dist/inspector/assets/{select-BCmhrYiM.js → select-DU7Jwzuy.js} +1 -1
  130. package/dist/inspector/assets/{settings-DRbR3maP.js → settings-BAPHY6RY.js} +1 -1
  131. package/dist/inspector/assets/{source_content-BuBSyT72.js → source_content-CIhKrGsl.js} +1 -1
  132. package/dist/inspector/assets/{source_content_open_button-DAYwX9Dh.js → source_content_open_button-Erw2ErgG.js} +1 -1
  133. package/dist/inspector/assets/{source_detail-DhEV5aJv.js → source_detail-C0XOo0Uu.js} +1 -1
  134. package/dist/inspector/assets/{source_link-CE1kUYaL.js → source_link-CkcRMEO3.js} +1 -1
  135. package/dist/inspector/assets/{sources-LZmXkGnn.js → sources-C0JBEINb.js} +1 -1
  136. package/dist/inspector/assets/{subscriptions-b_DO87jI.js → subscriptions-DxU4kHGt.js} +1 -1
  137. package/dist/inspector/assets/{switch-B6x__4FK.js → switch-BhKadp29.js} +1 -1
  138. package/dist/inspector/assets/{tabs-Bi6BUJqc.js → tabs-d81XObYb.js} +1 -1
  139. package/dist/inspector/assets/{textarea-3QlsHqkf.js → textarea-DTiKLH24.js} +1 -1
  140. package/dist/inspector/assets/{timeline-BoE9ZRrA.js → timeline-98QXcGln.js} +1 -1
  141. package/dist/inspector/assets/{timeline-a2L8UjV_.js → timeline-BEyKQpER.js} +1 -1
  142. package/dist/inspector/assets/{timeline_event_detail-D8eTp4i6.js → timeline_event_detail-DzGzqtrx.js} +1 -1
  143. package/dist/inspector/assets/{turn_detail-B2uAxNHK.js → turn_detail-CDP_FEPT.js} +1 -1
  144. package/dist/inspector/assets/{turns-BpYk8UFz.js → turns-DLmr1Iue.js} +1 -1
  145. package/dist/inspector/assets/{use_agents-Bt3UqR6w.js → use_agents-D3rvlr5v.js} +1 -1
  146. package/dist/inspector/assets/{use_entities-wuMfztjo.js → use_entities-CVikeUOv.js} +1 -1
  147. package/dist/inspector/assets/use_interpretations-CCoQ6ihs.js +1 -0
  148. package/dist/inspector/assets/{use_mutations-BhRvMBFs.js → use_mutations-BhwSLXcJ.js} +1 -1
  149. package/dist/inspector/assets/{use_peers-CD_9h7zQ.js → use_peers-QTm1Z_BV.js} +1 -1
  150. package/dist/inspector/assets/{use_relationships-CjsGAwR8.js → use_relationships-DtymxAI9.js} +1 -1
  151. package/dist/inspector/assets/{use_stats-IyFfTD4L.js → use_stats-BW1Z-dI2.js} +1 -1
  152. package/dist/inspector/assets/{use_timeline-BueAlCUW.js → use_timeline-DND87Sof.js} +1 -1
  153. package/dist/inspector/assets/{use_turns-CZtZ839A.js → use_turns-Dfb6Q8Ru.js} +1 -1
  154. package/dist/inspector/assets/{world_time_event_timeline-D8bL35LA.js → world_time_event_timeline-DxzejNrJ.js} +1 -1
  155. package/dist/inspector/index.html +2 -2
  156. package/dist/maintenance/entity_lifecycle_inventory.d.ts +31 -0
  157. package/dist/maintenance/entity_lifecycle_inventory.d.ts.map +1 -0
  158. package/dist/maintenance/entity_lifecycle_inventory.js +107 -0
  159. package/dist/maintenance/entity_lifecycle_inventory.js.map +1 -0
  160. package/dist/maintenance/entity_lifecycle_manifest.d.ts +84 -0
  161. package/dist/maintenance/entity_lifecycle_manifest.d.ts.map +1 -0
  162. package/dist/maintenance/entity_lifecycle_manifest.js +225 -0
  163. package/dist/maintenance/entity_lifecycle_manifest.js.map +1 -0
  164. package/dist/maintenance/entity_lifecycle_migration.d.ts +11 -0
  165. package/dist/maintenance/entity_lifecycle_migration.d.ts.map +1 -0
  166. package/dist/maintenance/entity_lifecycle_migration.js +576 -0
  167. package/dist/maintenance/entity_lifecycle_migration.js.map +1 -0
  168. package/dist/maintenance/entity_lifecycle_records.d.ts +12 -0
  169. package/dist/maintenance/entity_lifecycle_records.d.ts.map +1 -0
  170. package/dist/maintenance/entity_lifecycle_records.js +95 -0
  171. package/dist/maintenance/entity_lifecycle_records.js.map +1 -0
  172. package/dist/mcp_ws_bridge.js +26 -3
  173. package/dist/mcp_ws_bridge.js.map +1 -1
  174. package/dist/reducers/observation_reducer.d.ts +67 -3
  175. package/dist/reducers/observation_reducer.d.ts.map +1 -1
  176. package/dist/reducers/observation_reducer.js +202 -70
  177. package/dist/reducers/observation_reducer.js.map +1 -1
  178. package/dist/repositories/db/connection.d.ts +2 -0
  179. package/dist/repositories/db/connection.d.ts.map +1 -1
  180. package/dist/repositories/db/connection.js +27 -0
  181. package/dist/repositories/db/connection.js.map +1 -1
  182. package/dist/repositories/db/store_condition_schema.d.ts +3 -0
  183. package/dist/repositories/db/store_condition_schema.d.ts.map +1 -0
  184. package/dist/repositories/db/store_condition_schema.js +14 -0
  185. package/dist/repositories/db/store_condition_schema.js.map +1 -0
  186. package/dist/repositories/sqlite/local_db_adapter.d.ts +2 -0
  187. package/dist/repositories/sqlite/local_db_adapter.d.ts.map +1 -1
  188. package/dist/repositories/sqlite/local_db_adapter.js +21 -5
  189. package/dist/repositories/sqlite/local_db_adapter.js.map +1 -1
  190. package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
  191. package/dist/repositories/sqlite/sqlite_client.js +2 -0
  192. package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
  193. package/dist/repositories/sqlite/sqlite_driver.d.ts +10 -2
  194. package/dist/repositories/sqlite/sqlite_driver.d.ts.map +1 -1
  195. package/dist/repositories/sqlite/sqlite_driver.js +32 -5
  196. package/dist/repositories/sqlite/sqlite_driver.js.map +1 -1
  197. package/dist/repositories/worker/worker_file_database.d.ts +19 -0
  198. package/dist/repositories/worker/worker_file_database.d.ts.map +1 -1
  199. package/dist/repositories/worker/worker_file_database.js +23 -0
  200. package/dist/repositories/worker/worker_file_database.js.map +1 -1
  201. package/dist/server.d.ts +10 -1
  202. package/dist/server.d.ts.map +1 -1
  203. package/dist/server.js +550 -112
  204. package/dist/server.js.map +1 -1
  205. package/dist/services/agent_capabilities.d.ts +19 -4
  206. package/dist/services/agent_capabilities.d.ts.map +1 -1
  207. package/dist/services/agent_capabilities.js +60 -0
  208. package/dist/services/agent_capabilities.js.map +1 -1
  209. package/dist/services/agent_grants.d.ts.map +1 -1
  210. package/dist/services/agent_grants.js +19 -0
  211. package/dist/services/agent_grants.js.map +1 -1
  212. package/dist/services/array_item_patch.d.ts +158 -0
  213. package/dist/services/array_item_patch.d.ts.map +1 -0
  214. package/dist/services/array_item_patch.js +375 -0
  215. package/dist/services/array_item_patch.js.map +1 -0
  216. package/dist/services/attachment_resolution.d.ts.map +1 -1
  217. package/dist/services/attachment_resolution.js +2 -0
  218. package/dist/services/attachment_resolution.js.map +1 -1
  219. package/dist/services/attribution_redaction.d.ts.map +1 -1
  220. package/dist/services/attribution_redaction.js +16 -2
  221. package/dist/services/attribution_redaction.js.map +1 -1
  222. package/dist/services/batch_correction.d.ts +2 -0
  223. package/dist/services/batch_correction.d.ts.map +1 -1
  224. package/dist/services/batch_correction.js +19 -15
  225. package/dist/services/batch_correction.js.map +1 -1
  226. package/dist/services/canonical_markdown.d.ts +7 -23
  227. package/dist/services/canonical_markdown.d.ts.map +1 -1
  228. package/dist/services/canonical_markdown.js +10 -0
  229. package/dist/services/canonical_markdown.js.map +1 -1
  230. package/dist/services/correction.d.ts +118 -0
  231. package/dist/services/correction.d.ts.map +1 -1
  232. package/dist/services/correction.js +298 -36
  233. package/dist/services/correction.js.map +1 -1
  234. package/dist/services/correction_transaction.d.ts +27 -0
  235. package/dist/services/correction_transaction.d.ts.map +1 -0
  236. package/dist/services/correction_transaction.js +189 -0
  237. package/dist/services/correction_transaction.js.map +1 -0
  238. package/dist/services/dashboard_stats.d.ts.map +1 -1
  239. package/dist/services/dashboard_stats.js +8 -3
  240. package/dist/services/dashboard_stats.js.map +1 -1
  241. package/dist/services/deletion.d.ts.map +1 -1
  242. package/dist/services/deletion.js +235 -101
  243. package/dist/services/deletion.js.map +1 -1
  244. package/dist/services/entity_lifecycle_authority.d.ts +62 -0
  245. package/dist/services/entity_lifecycle_authority.d.ts.map +1 -0
  246. package/dist/services/entity_lifecycle_authority.js +210 -0
  247. package/dist/services/entity_lifecycle_authority.js.map +1 -0
  248. package/dist/services/entity_lifecycle_ingress.d.ts +15 -0
  249. package/dist/services/entity_lifecycle_ingress.d.ts.map +1 -0
  250. package/dist/services/entity_lifecycle_ingress.js +72 -0
  251. package/dist/services/entity_lifecycle_ingress.js.map +1 -0
  252. package/dist/services/entity_lifecycle_storage.d.ts +40 -0
  253. package/dist/services/entity_lifecycle_storage.d.ts.map +1 -0
  254. package/dist/services/entity_lifecycle_storage.js +415 -0
  255. package/dist/services/entity_lifecycle_storage.js.map +1 -0
  256. package/dist/services/entity_queries.d.ts +14 -1
  257. package/dist/services/entity_queries.d.ts.map +1 -1
  258. package/dist/services/entity_queries.js +117 -70
  259. package/dist/services/entity_queries.js.map +1 -1
  260. package/dist/services/entity_resolution.d.ts +24 -0
  261. package/dist/services/entity_resolution.d.ts.map +1 -1
  262. package/dist/services/entity_resolution.js +60 -4
  263. package/dist/services/entity_resolution.js.map +1 -1
  264. package/dist/services/entity_semantic_search.d.ts +3 -0
  265. package/dist/services/entity_semantic_search.d.ts.map +1 -1
  266. package/dist/services/entity_semantic_search.js +12 -4
  267. package/dist/services/entity_semantic_search.js.map +1 -1
  268. package/dist/services/entity_snapshot_at_time.d.ts +3 -1
  269. package/dist/services/entity_snapshot_at_time.d.ts.map +1 -1
  270. package/dist/services/entity_snapshot_at_time.js +71 -16
  271. package/dist/services/entity_snapshot_at_time.js.map +1 -1
  272. package/dist/services/entity_snapshot_embedding.d.ts +8 -1
  273. package/dist/services/entity_snapshot_embedding.d.ts.map +1 -1
  274. package/dist/services/entity_snapshot_embedding.js +60 -3
  275. package/dist/services/entity_snapshot_embedding.js.map +1 -1
  276. package/dist/services/entity_split.d.ts.map +1 -1
  277. package/dist/services/entity_split.js +4 -2
  278. package/dist/services/entity_split.js.map +1 -1
  279. package/dist/services/entity_version.d.ts +12 -0
  280. package/dist/services/entity_version.d.ts.map +1 -0
  281. package/dist/services/entity_version.js +17 -0
  282. package/dist/services/entity_version.js.map +1 -0
  283. package/dist/services/instance_policy.d.ts +3 -1
  284. package/dist/services/instance_policy.d.ts.map +1 -1
  285. package/dist/services/instance_policy.js +16 -2
  286. package/dist/services/instance_policy.js.map +1 -1
  287. package/dist/services/interpretation.d.ts +5 -0
  288. package/dist/services/interpretation.d.ts.map +1 -1
  289. package/dist/services/interpretation.js +3 -1
  290. package/dist/services/interpretation.js.map +1 -1
  291. package/dist/services/local_entity_embedding.d.ts +3 -0
  292. package/dist/services/local_entity_embedding.d.ts.map +1 -1
  293. package/dist/services/local_entity_embedding.js +14 -2
  294. package/dist/services/local_entity_embedding.js.map +1 -1
  295. package/dist/services/memory_export.d.ts.map +1 -1
  296. package/dist/services/memory_export.js +2 -1
  297. package/dist/services/memory_export.js.map +1 -1
  298. package/dist/services/observation_insert.d.ts +137 -0
  299. package/dist/services/observation_insert.d.ts.map +1 -0
  300. package/dist/services/observation_insert.js +124 -0
  301. package/dist/services/observation_insert.js.map +1 -0
  302. package/dist/services/observation_source_rank.d.ts +29 -0
  303. package/dist/services/observation_source_rank.d.ts.map +1 -0
  304. package/dist/services/observation_source_rank.js +37 -0
  305. package/dist/services/observation_source_rank.js.map +1 -0
  306. package/dist/services/observation_source_warning.d.ts +66 -0
  307. package/dist/services/observation_source_warning.d.ts.map +1 -0
  308. package/dist/services/observation_source_warning.js +71 -0
  309. package/dist/services/observation_source_warning.js.map +1 -0
  310. package/dist/services/observation_storage.d.ts +16 -11
  311. package/dist/services/observation_storage.d.ts.map +1 -1
  312. package/dist/services/observation_storage.js +25 -38
  313. package/dist/services/observation_storage.js.map +1 -1
  314. package/dist/services/provenance_data_origin.d.ts +81 -0
  315. package/dist/services/provenance_data_origin.d.ts.map +1 -0
  316. package/dist/services/provenance_data_origin.js +171 -0
  317. package/dist/services/provenance_data_origin.js.map +1 -0
  318. package/dist/services/raw_fragments.d.ts +2 -0
  319. package/dist/services/raw_fragments.d.ts.map +1 -1
  320. package/dist/services/raw_fragments.js +20 -4
  321. package/dist/services/raw_fragments.js.map +1 -1
  322. package/dist/services/raw_storage.d.ts.map +1 -1
  323. package/dist/services/raw_storage.js +3 -0
  324. package/dist/services/raw_storage.js.map +1 -1
  325. package/dist/services/relationship_write_capability.d.ts +85 -0
  326. package/dist/services/relationship_write_capability.d.ts.map +1 -0
  327. package/dist/services/relationship_write_capability.js +139 -0
  328. package/dist/services/relationship_write_capability.js.map +1 -0
  329. package/dist/services/relationships.d.ts.map +1 -1
  330. package/dist/services/relationships.js +20 -0
  331. package/dist/services/relationships.js.map +1 -1
  332. package/dist/services/root_landing/site_nav.d.ts.map +1 -1
  333. package/dist/services/root_landing/site_nav.js +1 -16
  334. package/dist/services/root_landing/site_nav.js.map +1 -1
  335. package/dist/services/schema_reference_linking.d.ts.map +1 -1
  336. package/dist/services/schema_reference_linking.js +39 -0
  337. package/dist/services/schema_reference_linking.js.map +1 -1
  338. package/dist/services/schema_registry.d.ts +22 -3
  339. package/dist/services/schema_registry.d.ts.map +1 -1
  340. package/dist/services/schema_registry.js +60 -3
  341. package/dist/services/schema_registry.js.map +1 -1
  342. package/dist/services/snapshot_computation.d.ts +6 -3
  343. package/dist/services/snapshot_computation.d.ts.map +1 -1
  344. package/dist/services/snapshot_computation.js +40 -11
  345. package/dist/services/snapshot_computation.js.map +1 -1
  346. package/dist/services/snapshot_export.d.ts.map +1 -1
  347. package/dist/services/snapshot_export.js +2 -1
  348. package/dist/services/snapshot_export.js.map +1 -1
  349. package/dist/services/stable_serialize.d.ts +19 -0
  350. package/dist/services/stable_serialize.d.ts.map +1 -0
  351. package/dist/services/stable_serialize.js +40 -0
  352. package/dist/services/stable_serialize.js.map +1 -0
  353. package/dist/services/store_admission.d.ts +34 -0
  354. package/dist/services/store_admission.d.ts.map +1 -0
  355. package/dist/services/store_admission.js +155 -0
  356. package/dist/services/store_admission.js.map +1 -0
  357. package/dist/services/store_combined_admission.d.ts +7 -0
  358. package/dist/services/store_combined_admission.d.ts.map +1 -0
  359. package/dist/services/store_combined_admission.js +59 -0
  360. package/dist/services/store_combined_admission.js.map +1 -0
  361. package/dist/services/store_condition_identity.d.ts +11 -0
  362. package/dist/services/store_condition_identity.d.ts.map +1 -0
  363. package/dist/services/store_condition_identity.js +44 -0
  364. package/dist/services/store_condition_identity.js.map +1 -0
  365. package/dist/services/store_condition_keys.d.ts +72 -0
  366. package/dist/services/store_condition_keys.d.ts.map +1 -0
  367. package/dist/services/store_condition_keys.js +249 -0
  368. package/dist/services/store_condition_keys.js.map +1 -0
  369. package/dist/services/store_condition_request.d.ts +2 -0
  370. package/dist/services/store_condition_request.d.ts.map +1 -0
  371. package/dist/services/store_condition_request.js +39 -0
  372. package/dist/services/store_condition_request.js.map +1 -0
  373. package/dist/services/store_conditional.d.ts +50 -0
  374. package/dist/services/store_conditional.d.ts.map +1 -0
  375. package/dist/services/store_conditional.js +392 -0
  376. package/dist/services/store_conditional.js.map +1 -0
  377. package/dist/services/store_mutation_authority.d.ts +4 -0
  378. package/dist/services/store_mutation_authority.d.ts.map +1 -0
  379. package/dist/services/store_mutation_authority.js +42 -0
  380. package/dist/services/store_mutation_authority.js.map +1 -0
  381. package/dist/services/store_ownership_admission.d.ts +3 -0
  382. package/dist/services/store_ownership_admission.d.ts.map +1 -0
  383. package/dist/services/store_ownership_admission.js +60 -0
  384. package/dist/services/store_ownership_admission.js.map +1 -0
  385. package/dist/services/store_write_state.d.ts +112 -0
  386. package/dist/services/store_write_state.d.ts.map +1 -0
  387. package/dist/services/store_write_state.js +254 -0
  388. package/dist/services/store_write_state.js.map +1 -0
  389. package/dist/services/sync/peer_sync_batch.d.ts.map +1 -1
  390. package/dist/services/sync/peer_sync_batch.js +2 -1
  391. package/dist/services/sync/peer_sync_batch.js.map +1 -1
  392. package/dist/services/timeline_events.d.ts +2 -0
  393. package/dist/services/timeline_events.d.ts.map +1 -1
  394. package/dist/services/timeline_events.js +2 -0
  395. package/dist/services/timeline_events.js.map +1 -1
  396. package/dist/shared/action_handlers/entity_handlers.d.ts +3 -0
  397. package/dist/shared/action_handlers/entity_handlers.d.ts.map +1 -1
  398. package/dist/shared/action_handlers/entity_handlers.js +280 -15
  399. package/dist/shared/action_handlers/entity_handlers.js.map +1 -1
  400. package/dist/shared/action_schemas.d.ts +683 -227
  401. package/dist/shared/action_schemas.d.ts.map +1 -1
  402. package/dist/shared/action_schemas.js +243 -8
  403. package/dist/shared/action_schemas.js.map +1 -1
  404. package/dist/shared/bind_host.d.ts +28 -0
  405. package/dist/shared/bind_host.d.ts.map +1 -0
  406. package/dist/shared/bind_host.js +34 -0
  407. package/dist/shared/bind_host.js.map +1 -0
  408. package/dist/shared/contract_mappings.d.ts.map +1 -1
  409. package/dist/shared/contract_mappings.js +27 -0
  410. package/dist/shared/contract_mappings.js.map +1 -1
  411. package/dist/shared/entity_read_contract.d.ts +55 -0
  412. package/dist/shared/entity_read_contract.d.ts.map +1 -0
  413. package/dist/shared/entity_read_contract.js +201 -0
  414. package/dist/shared/entity_read_contract.js.map +1 -0
  415. package/dist/shared/openapi_types.d.ts +986 -31
  416. package/dist/shared/openapi_types.d.ts.map +1 -1
  417. package/dist/tool_definitions.d.ts +1 -1
  418. package/dist/tool_definitions.d.ts.map +1 -1
  419. package/dist/tool_definitions.js +25 -4
  420. package/dist/tool_definitions.js.map +1 -1
  421. package/dist/utils/lucide_icons.d.ts +1 -1
  422. package/dist/utils/safe_request_log_format.d.ts +2 -0
  423. package/dist/utils/safe_request_log_format.d.ts.map +1 -1
  424. package/dist/utils/safe_request_log_format.js +53 -0
  425. package/dist/utils/safe_request_log_format.js.map +1 -1
  426. package/docs/README.md +49 -0
  427. package/docs/developer/mcp/instructions.md +29 -18
  428. package/openapi.yaml +1240 -22
  429. package/package.json +10 -2
  430. package/dist/inspector/assets/agent_grant_form-DoLjngLK.js +0 -1
  431. package/dist/inspector/assets/interpretations-CJF4giZE.js +0 -1
  432. package/dist/inspector/assets/record_activity-Bcv9OocC.js +0 -1
  433. 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, {
@@ -9350,6 +9527,15 @@ app.post("/delete_relationship", async (req, res) => {
9350
9527
  "one of the returned types.",
9351
9528
  });
9352
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
+ });
9353
9539
  const { softDeleteRelationship } = await import("./services/deletion.js");
9354
9540
  // Construct relationship key
9355
9541
  const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
@@ -9910,6 +10096,27 @@ app.post("/register_schema", async (req, res) => {
9910
10096
  }
9911
10097
  });
9912
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
+ });
9913
10120
  app.post("/correct", async (req, res) => {
9914
10121
  const parsed = CorrectEntityRequestSchema.safeParse(req.body);
9915
10122
  if (!parsed.success) {
@@ -9920,7 +10127,7 @@ app.post("/correct", async (req, res) => {
9920
10127
  }
9921
10128
  try {
9922
10129
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
9923
- 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;
9924
10131
  const correctCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
9925
10132
  if (correctCtx) {
9926
10133
  enforceAgentCapability("correct", [entity_type], correctCtx);
@@ -9937,9 +10144,9 @@ app.post("/correct", async (req, res) => {
9937
10144
  // propagates (it is NOT coerced to "declared field" — product principle
9938
10145
  // 10.2); a missing schema throws CorrectionSchemaNotFoundError, surfaced as
9939
10146
  // ERR_NO_SCHEMA_FOR_ENTITY_TYPE. Both reach the catch below.
9940
- const { createCorrection, resolveCorrectionSchema, buildCorrectionResponse } = await import("./services/correction.js");
10147
+ const { createCorrection, createCorrectionWithVersionPrecondition, resolveCorrectionSchema, buildCorrectionResponse, } = await import("./services/correction.js");
9941
10148
  const { schemaVersion: correctionSchemaVersion, isUnknownField } = await resolveCorrectionSchema(entity_type, field, userId);
9942
- const result = await createCorrection({
10149
+ const correctionParams = {
9943
10150
  entity_id,
9944
10151
  entity_type,
9945
10152
  field,
@@ -9947,7 +10154,14 @@ app.post("/correct", async (req, res) => {
9947
10154
  schema_version: correctionSchemaVersion,
9948
10155
  user_id: userId,
9949
10156
  idempotency_key,
9950
- });
10157
+ };
10158
+ const result = typeof expected_version === "string"
10159
+ ? await createCorrectionWithVersionPrecondition({
10160
+ ...correctionParams,
10161
+ expected_version,
10162
+ overwrite,
10163
+ })
10164
+ : await createCorrection(correctionParams);
9951
10165
  if (isUnknownField) {
9952
10166
  try {
9953
10167
  const { storeFragment } = await import("./services/raw_fragments.js");
@@ -9978,8 +10192,10 @@ app.post("/correct", async (req, res) => {
9978
10192
  entity_id,
9979
10193
  entity_type,
9980
10194
  field,
9981
- value,
10195
+ value: result.value,
9982
10196
  isUnknownField,
10197
+ replayed: result.replayed,
10198
+ entity_version: result.entity_version,
9983
10199
  }),
9984
10200
  });
9985
10201
  }
@@ -9988,13 +10204,43 @@ app.post("/correct", async (req, res) => {
9988
10204
  // (ERR_NO_SCHEMA_FOR_ENTITY_TYPE), not an internal failure. A schema-registry
9989
10205
  // IO failure is NOT swallowed here — it falls through to the generic 500
9990
10206
  // handler below instead of being coerced into a "declared field" outcome.
9991
- const { CorrectionSchemaNotFoundError } = await import("./services/correction.js");
10207
+ const { CorrectionSchemaNotFoundError, CorrectionEntityTypeMismatchError, CorrectionIdempotencyMismatchError, FieldVersionConflictError, } = await import("./services/correction.js");
9992
10208
  if (error instanceof CorrectionSchemaNotFoundError) {
9993
10209
  logWarn("ValidationError:correct", req, { code: error.code });
9994
10210
  return res
9995
10211
  .status(400)
9996
10212
  .json(buildErrorEnvelope(error.code, error.message, { entity_type: error.entityType }));
9997
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
+ }
9998
10244
  // Instance store-policy denial (#1975) — same envelope as /store, so a
9999
10245
  // caller handling one handles both.
10000
10246
  const { StorePolicyDeniedError } = await import("./services/instance_policy.js");
@@ -10008,6 +10254,131 @@ app.post("/correct", async (req, res) => {
10008
10254
  return handleApiError(req, res, error, "Failed to create correction", "DB_QUERY_FAILED", "APIError:correct");
10009
10255
  }
10010
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
+ });
10011
10382
  // POST /get_authenticated_user - Get authenticated user ID
10012
10383
  // FU-2026-05-003: GET /conversations/:conversation_id/turn-index — Inspector
10013
10384
  // per-turn anchor sections and turn timeline sidebar.
@@ -10823,14 +11194,12 @@ function assertRouteTableIsReachable() {
10823
11194
  * loopback while the socket was actually open on all interfaces).
10824
11195
  */
10825
11196
  export function resolveHttpBindHost(env = process.env) {
10826
- const raw = (env.NEOTOMA_HTTP_HOST || "").trim();
10827
- return raw.length > 0 ? raw : "127.0.0.1";
10828
- }
10829
- /** True when the given bind host resolves to a loopback-only address. */
10830
- export function isLoopbackHost(host) {
10831
- const normalized = host.trim().toLowerCase();
10832
- return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
11197
+ return resolveBindHostFromEnv("NEOTOMA_HTTP_HOST", env);
10833
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 };
10834
11203
  /**
10835
11204
  * Try to bind on a port/host; resolves with server and port, or rejects on
10836
11205
  * error (e.g. EADDRINUSE).