wednesdayai 0.4.12-dev.716 → 0.4.12-dev.719

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 (437) hide show
  1. package/dist/{agent-scope-cbxkoURo.js → agent-scope-DxKPMu8w.js} +3 -3
  2. package/dist/{agent-turn-enqueue-N-q0v3mc.js → agent-turn-enqueue-DEStltEg.js} +4 -4
  3. package/dist/{agent-turn-enqueue-Cqo34Swm.js → agent-turn-enqueue-DzWOPhgy.js} +4 -4
  4. package/dist/{agent-turn-execute-Bw9hhkAP.js → agent-turn-execute-CzOC4mHw.js} +124 -97
  5. package/dist/{agent-turn-recovery-adapter-BkIal4FW.js → agent-turn-recovery-adapter-CdLD3DuI.js} +1 -1
  6. package/dist/{agent-turn-recovery-adapter-CLY2Dn04.js → agent-turn-recovery-adapter-DnvjgbK0.js} +1 -1
  7. package/dist/{agent-turn-runner-Vd3aTCni.js → agent-turn-runner-BEvwtTis.js} +1 -1
  8. package/dist/{agent-turn-runner-Bvj00mHU.js → agent-turn-runner-BsxQBFRB.js} +5 -5
  9. package/dist/{agent-turn-runner-CUCOdxhH.js → agent-turn-runner-C1u8sVfA.js} +1 -1
  10. package/dist/{agent-turn-runner-CImqp_RN.js → agent-turn-runner-DpICdamt.js} +1 -1
  11. package/dist/{agents-BBoIqOw6.js → agents-C61lzbyw.js} +4 -4
  12. package/dist/{agents.config-Ctnm8o8d.js → agents.config-BcD-ZJ0N.js} +1 -1
  13. package/dist/{agents.config-C8RJrfbr.js → agents.config-DAh5Ah1J.js} +1 -1
  14. package/dist/{api-key-rotation-BMnyfyGD.js → api-key-rotation-Br7_jaAV.js} +2 -2
  15. package/dist/{artifacts-DUhYSSmK.js → artifacts-QHme-vnS.js} +4 -4
  16. package/dist/{audio-preflight-yuM9Q3qh.js → audio-preflight-B_5sTSyb.js} +27 -27
  17. package/dist/{audio-preflight-bEi-KkBn.js → audio-preflight-CehYz7wU.js} +6 -6
  18. package/dist/{audio-preflight-BKQpd1eU.js → audio-preflight-uatGs8bC.js} +6 -6
  19. package/dist/{audio-transcription-runner-CGTNNwbm.js → audio-transcription-runner-BvZFFknx.js} +1 -1
  20. package/dist/{audio-transcription-runner-BD7UgbNj.js → audio-transcription-runner-CJ8YdMv-.js} +1 -1
  21. package/dist/{audio-transcription-runner-CgPA8_eG.js → audio-transcription-runner-XFfvRPXl.js} +11 -11
  22. package/dist/{audit-CcNEpaG6.js → audit-C0WHA5BX.js} +3 -3
  23. package/dist/{audit-BM8WZyx6.js → audit-UMAK8rQC.js} +3 -3
  24. package/dist/{auth-choice-DTg-b2da.js → auth-choice-BHU_G3if.js} +1 -1
  25. package/dist/{auth-choice-VnJYar-i.js → auth-choice-CHzuLxTp.js} +1 -1
  26. package/dist/{banner-_g-nCJoZ.js → banner-BAtbMrrp.js} +1 -1
  27. package/dist/build-info.json +3 -3
  28. package/dist/bundled/boot-md/handler.js +58 -58
  29. package/dist/bundled/bootstrap-extra-files/handler.js +6 -6
  30. package/dist/bundled/command-logger/handler.js +2 -2
  31. package/dist/bundled/session-journal/handler.js +58 -58
  32. package/dist/bundled/session-memory/handler.js +58 -58
  33. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  34. package/dist/{channel-activity-BJHm_pDV.js → channel-activity-DNq7LYjU.js} +3 -3
  35. package/dist/{channel-options-BX8MFfPW.js → channel-options-CsgHOF5l.js} +1 -1
  36. package/dist/{channel-options-Dod3uWTl.js → channel-options-DRnowLFK.js} +1 -1
  37. package/dist/{channels-cli-MbAArXF1.js → channels-cli-BN4oTmr6.js} +23 -23
  38. package/dist/{channels-cli-DyfFtAZa.js → channels-cli-Cn1IUfM3.js} +23 -23
  39. package/dist/{chrome-BkJgMZjT.js → chrome-FHxGdLnd.js} +23 -23
  40. package/dist/{chunk-eNEsWhab.js → chunk-BhntPUH_.js} +3 -3
  41. package/dist/cli/daemon-cli.js +1 -1
  42. package/dist/{cli-DF91S8Cf.js → cli-BGAvAUiU.js} +17 -17
  43. package/dist/{cli-CdrO5h4P.js → cli-CFIRmNaZ.js} +17 -17
  44. package/dist/{command-queue-DmXWCoY0.js → command-queue-DtybAhHH.js} +1 -1
  45. package/dist/{command-queue-DucYxKKF.js → command-queue-SzKgkaS6.js} +1 -1
  46. package/dist/{command-queue-URs9pYSC.js → command-queue-zXu22yak.js} +1 -1
  47. package/dist/{command-registry-NOHWjbDr.js → command-registry-DBfEAwIt.js} +9 -9
  48. package/dist/{commands-registry-DdQg21ct.js → commands-registry-DC0fovy_.js} +2 -2
  49. package/dist/{completion-cli-BfKoz1jW.js → completion-cli-BfRsix3J.js} +2 -2
  50. package/dist/{completion-cli-BQ5pz74O.js → completion-cli-L5YP0rxV.js} +1 -1
  51. package/dist/{config-cli-cFzUpYdg.js → config-cli-D4Gxd0pm.js} +1 -1
  52. package/dist/{config-cli-BGFZu6JD.js → config-cli-dgRsXbD3.js} +1 -1
  53. package/dist/{configure-DYAZJ3GF.js → configure-BN-JNKf7.js} +5 -5
  54. package/dist/{configure-DrrwPxlu.js → configure-DtRyMLwO.js} +5 -5
  55. package/dist/{daemon-cli-B3Q460ai.js → daemon-cli-CiPY543O.js} +5 -5
  56. package/dist/{daemon-cli-C5cAb15Z.js → daemon-cli-YmPcKhvo.js} +5 -5
  57. package/dist/daemon-cli.js +44 -3
  58. package/dist/{daemon-install-DGU1qIIK.js → daemon-install-BuETscSF.js} +3 -3
  59. package/dist/{daemon-install-CBIxgo4p.js → daemon-install-CGg5r1wy.js} +3 -3
  60. package/dist/{deliver-C5SDo7TP.js → deliver-Aw7d03gL.js} +1 -1
  61. package/dist/{deliver-fwGuXC0n.js → deliver-CXqNQ8yi.js} +24 -24
  62. package/dist/{deliver-8xg5ISEI.js → deliver-D37woOwV.js} +1 -1
  63. package/dist/{diagnostic-BB6k_yiT.js → diagnostic-Bbg-zObh.js} +5 -5
  64. package/dist/{diagnostic-D7DvV7NT.js → diagnostic-Cid_Tw67.js} +1 -1
  65. package/dist/{diagnostic-Dv8UWiI0.js → diagnostic-lG00b_zg.js} +1 -1
  66. package/dist/{diagnostics-C4-TUVnm.js → diagnostics-C6ntZHc3.js} +1 -1
  67. package/dist/{diagnostics-FIcvxigK.js → diagnostics-Ds3p_NPY.js} +1 -1
  68. package/dist/{dm-policy-shared-BLJbsZzf.js → dm-policy-shared-BJNH9gNN.js} +1 -1
  69. package/dist/{doctor-completion-B94pFOFt.js → doctor-completion-ChF4A40n.js} +1 -1
  70. package/dist/{doctor-completion-CtR8yetH.js → doctor-completion-DIFx6d6S.js} +1 -1
  71. package/dist/entry.js +2 -2
  72. package/dist/{exec-approvals-cli-t7rAulRf.js → exec-approvals-cli-BAJ3sxzP.js} +3 -3
  73. package/dist/{exec-approvals-cli-DsXabPF_.js → exec-approvals-cli-DIUYxvM0.js} +3 -3
  74. package/dist/extensionAPI.js +2 -2
  75. package/dist/{fetch-BzXaK9px.js → fetch-BoHKSk-O.js} +8 -8
  76. package/dist/{fetch-guard-0mkjBgri.js → fetch-guard-TJjLK0FV.js} +3 -3
  77. package/dist/{frontmatter-PMByWrfP.js → frontmatter-saAwiKEb.js} +5 -5
  78. package/dist/{fs-safe-DwICJjkN.js → fs-safe-LRpYbPaA.js} +4 -4
  79. package/dist/{gateway-cli-C_veSlCx.js → gateway-cli-BJ9wa3Hc.js} +48 -48
  80. package/dist/{gateway-cli-DI7_gxH-.js → gateway-cli-CVnksdlt.js} +48 -48
  81. package/dist/{github-copilot-token-WSjo05vB.js → github-copilot-token-zGOMIGSh.js} +7 -7
  82. package/dist/{health-Cooqit6V.js → health-Cxv6ou16.js} +2 -2
  83. package/dist/{health-DFat5vO5.js → health-Q4hCJ8O2.js} +2 -2
  84. package/dist/{hooks-cli-CB-96Su8.js → hooks-cli-ChZY2urb.js} +19 -19
  85. package/dist/{hooks-cli-yNAgxRQS.js → hooks-cli-DOHX--ha.js} +19 -19
  86. package/dist/{image-ops-O8pHxo7i.js → image-ops-D_fm2PuR.js} +1 -1
  87. package/dist/index.js +21 -21
  88. package/dist/{inspect-DVHAPbcZ.js → inspect-BXlZnWkg.js} +1 -1
  89. package/dist/{inspect-gvjq38z1.js → inspect-LfUprDjx.js} +1 -1
  90. package/dist/{installs-CpnJwZZ_.js → installs-78zhoWuO.js} +1 -1
  91. package/dist/{installs-CdLdL1gD.js → installs-Du8n8Do9.js} +1 -1
  92. package/dist/{ir-fzpdYPWw.js → ir-C4MMLUJr.js} +5 -5
  93. package/dist/{legacy-names-CLlrMq8r.js → legacy-names-vLJrYADD.js} +1 -1
  94. package/dist/{lifecycle-core-BdCcww82.js → lifecycle-core-CuMY0U_8.js} +3 -3
  95. package/dist/{lifecycle-core-B3-ezmTx.js → lifecycle-core-M-5Z8Xml.js} +3 -3
  96. package/dist/llm-slug-generator.js +58 -58
  97. package/dist/{logger-DNehzD_s.js → logger-a0-zAY5a.js} +1 -1
  98. package/dist/{manager-DH-MILAU.js → manager-DdA_cy40.js} +12 -12
  99. package/dist/{markdown-tables-UWspJNAH.js → markdown-tables-Dd7jV1JF.js} +2 -2
  100. package/dist/{model-selection-9qbqwsOT.js → model-selection-CdZITU0t.js} +86 -86
  101. package/dist/{models-Dlat36jl.js → models-BbhdUY5g.js} +1 -1
  102. package/dist/{models-cli-vjFaOcNS.js → models-cli-BekoSHw2.js} +18 -18
  103. package/dist/{models-cli-DMjHkAqq.js → models-cli-BmL2dpSq.js} +17 -17
  104. package/dist/{node-cli-yC-cX-mJ.js → node-cli-BTywy8AL.js} +8 -8
  105. package/dist/{node-cli-m0j_YD0V.js → node-cli-BVgH-DXq.js} +8 -8
  106. package/dist/{node-service-BnwLXY5x.js → node-service-BnyKZNqa.js} +1 -1
  107. package/dist/{node-service-r64yexlb.js → node-service-Ys7JZFz-.js} +1 -1
  108. package/dist/{npm-pack-install-DcBobJp3.js → npm-pack-install-B-cOWuiF.js} +1 -1
  109. package/dist/{npm-pack-install-B7wQANiM.js → npm-pack-install-opyKYSmS.js} +1 -1
  110. package/dist/{npm-resolution-B0rqOBgy.js → npm-resolution-51BBOWpQ.js} +2 -2
  111. package/dist/{npm-resolution-SdoNL0ud.js → npm-resolution-6WKQ6nBu.js} +2 -2
  112. package/dist/{onboard-B9-DxI_m.js → onboard-Cj-sLY2K.js} +3 -3
  113. package/dist/{onboard-D8MZoOum.js → onboard-CnRiqboN.js} +3 -3
  114. package/dist/{onboard-channels-P7F-dNwm.js → onboard-channels-DAwHVQ1P.js} +2 -2
  115. package/dist/{onboard-channels-28JqVvAv.js → onboard-channels-DYRTySsa.js} +2 -2
  116. package/dist/{onboarding-BfpXrIw7.js → onboarding-CujUbWy2.js} +3 -3
  117. package/dist/{onboarding-0WSY8o8f.js → onboarding-DidWWI3X.js} +3 -3
  118. package/dist/{onboarding.finalize-QITC8MsS.js → onboarding.finalize-CDE1nAu8.js} +26 -26
  119. package/dist/{onboarding.finalize-BnIw1M2b.js → onboarding.finalize-DTqRxVZH.js} +25 -25
  120. package/dist/{operator-commands-CtYCnspr.js → operator-commands-Bfd55UfE.js} +2 -2
  121. package/dist/{operator-commands-CIz47IKl.js → operator-commands-Ct0WXISe.js} +2 -2
  122. package/dist/{outbound-attachment-DVKqk30B.js → outbound-attachment-CqtMGpsJ.js} +2 -2
  123. package/dist/{outbound-message-hooks-S_VhlJs3.js → outbound-message-hooks-RBQbGzGm.js} +4 -4
  124. package/dist/{path-alias-guards-Cd5yHmTq.js → path-alias-guards-Zj-NwKjV.js} +1 -1
  125. package/dist/{paths-Cvc9EM8Y.js → paths-C6TxBCvO.js} +5 -5
  126. package/dist/{pi-embedded-wjGM_bNN.js → pi-embedded-B6sr5IdC.js} +102 -75
  127. package/dist/{pi-embedded-BBId5n_k.js → pi-embedded-B9bOSjfE.js} +279 -252
  128. package/dist/{pi-embedded-helpers-BU47jdPO.js → pi-embedded-helpers-BAjtRn7m.js} +1 -1
  129. package/dist/{pi-embedded-helpers-DVvJRC5S.js → pi-embedded-helpers-BYLOFsUc.js} +24 -24
  130. package/dist/{pi-embedded-helpers-i0-tPaxH.js → pi-embedded-helpers-CPorV-Mk.js} +1 -1
  131. package/dist/{pi-embedded-recovery-adapter-CNdSU-s7.js → pi-embedded-recovery-adapter-B8Goyik3.js} +8 -8
  132. package/dist/{pi-embedded-recovery-adapter-DL78oCTf.js → pi-embedded-recovery-adapter-Cs1EJElw.js} +8 -8
  133. package/dist/{pi-embedded-recovery-adapter-1Lgt8q1h.js → pi-embedded-recovery-adapter-ZPyX9YC1.js} +2 -2
  134. package/dist/{pi-embedded-recovery-adapter-B2AFWrYt.js → pi-embedded-recovery-adapter-swbNIWgT.js} +2 -2
  135. package/dist/{pi-embedded-utils-8OzPnJMh.js → pi-embedded-utils-CT_f_S3Y.js} +1 -1
  136. package/dist/{pi-embedded-utils-BoaQCwP7.js → pi-embedded-utils-DOtaZ7KV.js} +3 -3
  137. package/dist/{pi-embedded-utils-BtOzugyp.js → pi-embedded-utils-lp-U3XQ9.js} +1 -1
  138. package/dist/{pi-model-discovery-CsC6yTDG.js → pi-model-discovery-B9KKtGuw.js} +7 -7
  139. package/dist/{pi-tools.policy-DPm2eos4.js → pi-tools.policy-r9H_A-NQ.js} +1 -1
  140. package/dist/{plugin-registry-CPS5bO_-.js → plugin-registry-CwGfmKsn.js} +1 -1
  141. package/dist/{plugin-registry-CC8YsXPr.js → plugin-registry-EHKfjAoe.js} +1 -1
  142. package/dist/plugin-sdk/{agent-scope-BYp_CjdU.js → agent-scope-BLF4nZUv.js} +2 -2
  143. package/dist/plugin-sdk/{agent-turn-execute-Dd5AJiKY.js → agent-turn-execute-BVPDW02i.js} +2 -2
  144. package/dist/plugin-sdk/agent-turn-execute-zuw0S_Lh.js +61 -0
  145. package/dist/plugin-sdk/agent-turn-runner-BcrkoR1M.js +11 -0
  146. package/dist/plugin-sdk/{agent-turn-runner-DL5jOnXC.js → agent-turn-runner-DDIT9POj.js} +1 -1
  147. package/dist/plugin-sdk/{agent-turn-runner-B9_FKVSu.js → agent-turn-runner-DjK-LdTA.js} +6 -6
  148. package/dist/plugin-sdk/{api-key-rotation-CDNf_4cI.js → api-key-rotation-XDUKz2gd.js} +2 -2
  149. package/dist/plugin-sdk/{artifacts-BMLMg3MG.js → artifacts-HAYpGlG1.js} +1 -1
  150. package/dist/plugin-sdk/{audio-preflight-BmKL6FNP.js → audio-preflight-BS29OWk3.js} +2 -2
  151. package/dist/plugin-sdk/audio-preflight-DE04bi4M.js +28 -0
  152. package/dist/plugin-sdk/{audio-transcription-runner-TBjJv_mZ.js → audio-transcription-runner-BNafitZj.js} +10 -10
  153. package/dist/plugin-sdk/{auth-profiles-DHZjW8Vk.js → auth-profiles-CCJQoedi.js} +5 -5
  154. package/dist/plugin-sdk/{channel-activity-Dr4PYej7.js → channel-activity-BKwRzYJb.js} +3 -3
  155. package/dist/plugin-sdk/{chrome-BDy1zYAr.js → chrome-D0eLSqkP.js} +4 -4
  156. package/dist/plugin-sdk/{chunk-DGQdf0JN.js → chunk-CDvvv9SU.js} +3 -3
  157. package/dist/plugin-sdk/{chunk-BTSnO1wn.js → chunk-DI-5pBck.js} +6 -6
  158. package/dist/plugin-sdk/{command-detection-DdTnFMU3.js → command-detection-WNHSrul-.js} +186 -159
  159. package/dist/plugin-sdk/{command-queue-CeaLVs8O.js → command-queue-BnJsJc_w.js} +8 -8
  160. package/dist/plugin-sdk/{command-queue-DFSOdqCK.js → command-queue-CHiQj9Ji.js} +1 -1
  161. package/dist/plugin-sdk/{commands-registry-8sy0wsK_.js → commands-registry-Do_g2It_.js} +6 -6
  162. package/dist/plugin-sdk/{commands-registry-N6f_lsxF.js → commands-registry-DpLk11E7.js} +2 -2
  163. package/dist/plugin-sdk/{config-Biusjzx-.js → config-CQNfmIC8.js} +7 -7
  164. package/dist/plugin-sdk/deliver-D4fWK65M.js +32 -0
  165. package/dist/plugin-sdk/{deliver-BgHA8c9F.js → deliver-L6eu6zyG.js} +11 -11
  166. package/dist/plugin-sdk/{diagnostic-D6hSjxiv.js → diagnostic-C7lPoqyv.js} +5 -5
  167. package/dist/plugin-sdk/{diagnostic-Bni_8VyY.js → diagnostic-DfxuMWHd.js} +7 -7
  168. package/dist/plugin-sdk/{fetch-guard-C1a1KfEb.js → fetch-guard-CZT8FU1i.js} +3 -3
  169. package/dist/plugin-sdk/{fs-safe-CbZ6uRVv.js → fs-safe-B0JgGdXa.js} +3 -3
  170. package/dist/plugin-sdk/{hook-runner-global-BvxlnAyu.js → hook-runner-global-WRI53ibi.js} +3 -3
  171. package/dist/plugin-sdk/{image-ops-DY_SZfXk.js → image-ops-kuILYHZv.js} +1 -1
  172. package/dist/plugin-sdk/index.js +54 -54
  173. package/dist/plugin-sdk/{ir-BNohi6ZW.js → ir-Bm7oqkwT.js} +5 -5
  174. package/dist/plugin-sdk/{local-roots-Bm7nDW20.js → local-roots-D-CHzVr1.js} +7 -7
  175. package/dist/plugin-sdk/{logger-D6KsqbNW.js → logger-BSK-0Yjx.js} +1 -1
  176. package/dist/plugin-sdk/manager-BlYRVNJI.js +16 -0
  177. package/dist/plugin-sdk/{manager-B_vK9MbM.js → manager-D2jffdjH.js} +8 -8
  178. package/dist/plugin-sdk/{markdown-tables-G4vK24xc.js → markdown-tables-DIWQQ2W1.js} +2 -2
  179. package/dist/plugin-sdk/markdown-tables-cZSKCBuW.js +10 -0
  180. package/dist/plugin-sdk/{outbound-attachment-Cw7T2Mrt.js → outbound-attachment-CtKKNyms.js} +2 -2
  181. package/dist/plugin-sdk/{path-alias-guards-CVybi_GQ.js → path-alias-guards-CHfCMaeX.js} +1 -1
  182. package/dist/plugin-sdk/{pi-embedded-helpers-B_jgacW8.js → pi-embedded-helpers-BRymkO_1.js} +11 -11
  183. package/dist/plugin-sdk/{pi-embedded-utils-DOXEm_H8.js → pi-embedded-utils-BHmc3RVS.js} +3 -3
  184. package/dist/plugin-sdk/{pi-model-discovery-DWw1ReX-.js → pi-model-discovery-4X7_2_nP.js} +6 -6
  185. package/dist/plugin-sdk/{pi-model-discovery-DWUrLRLg.js → pi-model-discovery-6BBajZds.js} +1 -1
  186. package/dist/plugin-sdk/provider-dispatcher-B4PdTha5.js +61 -0
  187. package/dist/plugin-sdk/{provider-dispatcher-C4-urx2x.js → provider-dispatcher-D7UK3G8K.js} +2 -2
  188. package/dist/plugin-sdk/provider-image-CNlYbRab.js +22 -0
  189. package/dist/plugin-sdk/{provider-image-CEQrHWYz.js → provider-image-DBye3Ln5.js} +5 -5
  190. package/dist/plugin-sdk/provider-media-understanding.js +7 -7
  191. package/dist/plugin-sdk/proxy-D2uiu68M.js +7 -0
  192. package/dist/plugin-sdk/{proxy-BWc9guJs.js → proxy-DJ2nskkh.js} +1 -1
  193. package/dist/plugin-sdk/{proxy-fetch-BYkGxQnh.js → proxy-fetch-CuQpjpkI.js} +1 -1
  194. package/dist/plugin-sdk/{pw-ai-DQv6Xhce.js → pw-ai-BJrhPMHJ.js} +10 -10
  195. package/dist/plugin-sdk/{qmd-manager-CpV0SFnY.js → qmd-manager-DhW_frb3.js} +8 -8
  196. package/dist/plugin-sdk/{query-expansion-CwmHvvV_.js → query-expansion-NpBeKA8F.js} +3 -3
  197. package/dist/plugin-sdk/{recorder-BXZMRSjO.js → recorder-DdbAQ2HO.js} +4 -4
  198. package/dist/plugin-sdk/{redact-C88Ryyfs.js → redact-CUjmZn2Y.js} +1 -1
  199. package/dist/plugin-sdk/{replies-l2hZAzbt.js → replies-Dw3jvqpR.js} +4 -4
  200. package/dist/plugin-sdk/replies-XUkqz_7S.js +25 -0
  201. package/dist/plugin-sdk/{reply-directives-BwxzQXmX.js → reply-directives-BS8Ge06Z.js} +2 -2
  202. package/dist/plugin-sdk/{reply-prefix-qLQRxqrf.js → reply-prefix-Dt1hESHJ.js} +1 -1
  203. package/dist/plugin-sdk/reply-prefix-MU3uY52y.js +7 -0
  204. package/dist/plugin-sdk/{resolve-route-50KoArP9.js → resolve-route-BpLuk-wC.js} +4 -4
  205. package/dist/plugin-sdk/{resolve-route-6v1HVvWk.js → resolve-route-DiQTL1qI.js} +6 -6
  206. package/dist/plugin-sdk/{runs-CiohhBb8.js → runs-D6fAvItC.js} +7 -7
  207. package/dist/plugin-sdk/runs-R28wdt7j.js +25 -0
  208. package/dist/plugin-sdk/{send-Cv2CGnsK.js → send-BDwTyH6I.js} +8 -8
  209. package/dist/plugin-sdk/{send-DM6RpDCL.js → send-CE_6VOiQ.js} +7 -7
  210. package/dist/plugin-sdk/{send-Cx6gHjnA.js → send-Ce2XBaKF.js} +6 -6
  211. package/dist/plugin-sdk/send-Cg2VXcO0.js +27 -0
  212. package/dist/plugin-sdk/{send-BMSqETcU.js → send-CpXGZgrS.js} +6 -6
  213. package/dist/plugin-sdk/send-DAs6FmWB.js +23 -0
  214. package/dist/plugin-sdk/{send-DO0C4Uva.js → send-GMYUhBtJ.js} +19 -19
  215. package/dist/plugin-sdk/{send-DG4MQPPm.js → send-R0HB1-l-.js} +13 -13
  216. package/dist/plugin-sdk/send-UIQH_kN6.js +25 -0
  217. package/dist/plugin-sdk/send-tASIQF-Q.js +24 -0
  218. package/dist/plugin-sdk/{session-meta-CiqJP0pO.js → session-meta-BU2WXD0N.js} +2 -2
  219. package/dist/plugin-sdk/session-meta-Dd1ZONel.js +10 -0
  220. package/dist/plugin-sdk/{skill-commands-CE_SkRQk.js → skill-commands-BYfWIPfV.js} +5 -5
  221. package/dist/plugin-sdk/skill-commands-C7ZE3rKj.js +14 -0
  222. package/dist/plugin-sdk/{skills-CjW6uGTa.js → skills-BuJjEYto.js} +6 -6
  223. package/dist/plugin-sdk/src/agents/pi-embedded-runner/run/attempt.d.ts +3 -0
  224. package/dist/plugin-sdk/src/agents/pi-embedded-runner/tool-result-context-guard.d.ts +12 -3
  225. package/dist/plugin-sdk/{ssrf-BUCEDxp5.js → ssrf-DiAz1Z_z.js} +2 -2
  226. package/dist/plugin-sdk/{store-yBNTHg3N.js → store-Bl3Yjf_w.js} +2 -2
  227. package/dist/plugin-sdk/{subagent-registry-B0gD-Lof.js → subagent-registry-Bxwla-0-.js} +2 -2
  228. package/dist/plugin-sdk/subagent-registry-CTZBVVPF.js +61 -0
  229. package/dist/plugin-sdk/{subsystem-CUUKL6Fr.js → subsystem-B2x33s9h.js} +4 -4
  230. package/dist/plugin-sdk/{tables-DKYi6grj.js → tables-CTmQQyfU.js} +1 -1
  231. package/dist/plugin-sdk/testing.js +2 -2
  232. package/dist/plugin-sdk/{tokens-ysZUkYZZ.js → tokens-DxJSOoJn.js} +1 -1
  233. package/dist/plugin-sdk/{tool-loop-detection-LsvZ1dIY.js → tool-loop-detection-Buouaxew.js} +1 -1
  234. package/dist/plugin-sdk/{tools-PVG4MyCb.js → tools-iyXmO6p1.js} +106 -79
  235. package/dist/plugin-sdk/user-run-recording-BMikDBXR.js +11 -0
  236. package/dist/plugin-sdk/{user-run-recording-BMaK7jNR.js → user-run-recording-juCW_uQ-.js} +2 -2
  237. package/dist/{plugins-cli-yjyVyPjc.js → plugins-cli-B0I1Xkrz.js} +22 -22
  238. package/dist/{plugins-cli-Cpq4XXoA.js → plugins-cli-CmYpruM_.js} +22 -22
  239. package/dist/{program-CLZlTcBu.js → program-CA1rDqtK.js} +23 -23
  240. package/dist/{program-context-DN3ucEgK.js → program-context-BCo6By7P.js} +24 -24
  241. package/dist/{prompt-select-styled-CtLCU2VG.js → prompt-select-styled-BAmwLkut.js} +12 -12
  242. package/dist/{prompt-select-styled-BNUoeaMg.js → prompt-select-styled-cL1dmYcO.js} +12 -12
  243. package/dist/{provider-image-Bi2L9LWh.js → provider-image-BllyLBJM.js} +5 -5
  244. package/dist/{provider-image-CZlpEJVI.js → provider-image-CIpRvhkS.js} +1 -1
  245. package/dist/{provider-image-UXLX6qTH.js → provider-image-D9B_J5Ys.js} +1 -1
  246. package/dist/{proxy-env-i0jn5uxB.js → proxy-env-CzGKpJY_.js} +2 -2
  247. package/dist/{proxy-C9sq6M-h.js → proxy-fWL7CKDP.js} +1 -1
  248. package/dist/{proxy-fetch-CK5AQAYw.js → proxy-fetch-DCI0KuH6.js} +1 -1
  249. package/dist/{push-apns-DYlVj3Sd.js → push-apns-Bhf8qK68.js} +2 -2
  250. package/dist/{push-apns-uBiZYT2e.js → push-apns-BmrddV2R.js} +2 -2
  251. package/dist/{pw-ai-CZJk6UyG.js → pw-ai-CKjrjwfw.js} +1 -1
  252. package/dist/{pw-ai-pYtWxKPR.js → pw-ai-CXSJw34w.js} +14 -14
  253. package/dist/{pw-ai-2o8dgziH.js → pw-ai-Cd08hlBj.js} +1 -1
  254. package/dist/{qmd-manager-p036XU01.js → qmd-manager-BwZ0V5fL.js} +10 -10
  255. package/dist/{query-expansion-30x7RlZY.js → query-expansion-31VWpPCz.js} +5 -5
  256. package/dist/{reconcile-bundled-Dx64x8xr.js → reconcile-bundled-Bl6XpmI5.js} +1 -1
  257. package/dist/{reconcile-bundled-oQwTCU_A.js → reconcile-bundled-TswjG3xv.js} +1 -1
  258. package/dist/{recorder-C_ViZLf1.js → recorder-DNRxe9C8.js} +4 -4
  259. package/dist/{recovery-adapter-BvRiQZIb.js → recovery-adapter-DOizlZvv.js} +8 -8
  260. package/dist/{recovery-adapter-BcPtMfaC.js → recovery-adapter-DW0R_vFE.js} +1 -1
  261. package/dist/{recovery-adapter-BT7EyQzh.js → recovery-adapter-DyPV7syS.js} +8 -8
  262. package/dist/{recovery-adapter-CTqGLqLj.js → recovery-adapter-mi8jbyvy.js} +1 -1
  263. package/dist/{redact-BOHmCPP_.js → redact-DZne_lu8.js} +1 -1
  264. package/dist/{register.agent-DpYKNZhv.js → register.agent-B3BSgQcW.js} +22 -22
  265. package/dist/{register.agent-DxsC-Szl.js → register.agent-CSHtI9uN.js} +23 -23
  266. package/dist/{register.configure-CiYkPB_K.js → register.configure-CYwDM9d3.js} +26 -26
  267. package/dist/{register.configure-Ceru0oh0.js → register.configure-Cu4CPpm4.js} +26 -26
  268. package/dist/{register.maintenance-BCSq69vI.js → register.maintenance-BXiHFIyG.js} +28 -28
  269. package/dist/{register.maintenance-D-kzdtog.js → register.maintenance-C6vjRY-I.js} +29 -29
  270. package/dist/{register.message-B3NWvxgd.js → register.message-BsBClRov.js} +18 -18
  271. package/dist/{register.message-D-e6_UXf.js → register.message-DNpBTvto.js} +18 -18
  272. package/dist/{register.onboard-BPko9BHz.js → register.onboard-BKQLGJhh.js} +2 -2
  273. package/dist/{register.onboard-BHBpxcRv.js → register.onboard-t_7lOkRu.js} +2 -2
  274. package/dist/{register.setup-BE-sR-Dc.js → register.setup-B7QHn0Qv.js} +2 -2
  275. package/dist/{register.setup-Bjx48C4T.js → register.setup-DEg7fSPZ.js} +2 -2
  276. package/dist/{register.status-health-sessions-CQQWiYcu.js → register.status-health-sessions-7E-y6cw7.js} +24 -24
  277. package/dist/{register.status-health-sessions-BczZXjHB.js → register.status-health-sessions-D9iuSOb8.js} +24 -24
  278. package/dist/{register.subclis-Bn9Re4fV.js → register.subclis-R6keQnon.js} +16 -16
  279. package/dist/{replies-BsctLhfp.js → replies-NkETqb6B.js} +4 -4
  280. package/dist/{reply-BzUdF5qW.js → reply-B4TgvJrJ.js} +124 -97
  281. package/dist/{reply-directives-D9wYtPPy.js → reply-directives-DGITtNUL.js} +2 -2
  282. package/dist/{reply-prefix-C7WlN3qm.js → reply-prefix-DVkNDBNe.js} +1 -1
  283. package/dist/{resolve-route-BV8k5qjX.js → resolve-route-jDAjkfZE.js} +5 -5
  284. package/dist/{run-main-Cfccgza4.js → run-main-CMTfGYjG.js} +30 -30
  285. package/dist/{runs-DVNBpgv0.js → runs-B0TxOQnF.js} +3 -3
  286. package/dist/{runs-i5IuTzQX.js → runs-BMp7jOLL.js} +3 -3
  287. package/dist/{runs-BauIdJqy.js → runs-DGWNXyKA.js} +8 -8
  288. package/dist/{sandbox-QsMPF2u6.js → sandbox-CQwpubTx.js} +1 -1
  289. package/dist/{sandbox-bbN1dfIo.js → sandbox-DbfWdQha.js} +1 -1
  290. package/dist/{sandbox-cli-CuG1HCrw.js → sandbox-cli-CSjghLhl.js} +2 -2
  291. package/dist/{sandbox-cli-nx3ctiYX.js → sandbox-cli-DwhgE7u4.js} +2 -2
  292. package/dist/{security-cli-BmfAwh-0.js → security-cli-DSB1NLa6.js} +4 -4
  293. package/dist/{security-cli-BfZEQFxV.js → security-cli-DlMcCXl1.js} +4 -4
  294. package/dist/{send-Tx-ew59_.js → send-CNLmncRW.js} +6 -6
  295. package/dist/{send-CvkM5xAx.js → send-DAYMV49d.js} +1 -1
  296. package/dist/{send-Dd2SE0h0.js → send-DEFcgo-1.js} +27 -27
  297. package/dist/{send-BGps2i_C.js → send-Do1AlhD5.js} +8 -8
  298. package/dist/{send-BiDcVZSc.js → send-Dq8ChBfS.js} +6 -6
  299. package/dist/{send-Ba3pDq6E.js → send-ENMm5qbW.js} +7 -7
  300. package/dist/{send-BKkvuHI9.js → send-l57G-s8y.js} +1 -1
  301. package/dist/{server-BMSgu_SV.js → server-BAGUd2MH.js} +4 -4
  302. package/dist/{server-DRVR_pkr.js → server-IDopagLT.js} +4 -4
  303. package/dist/{server-context-CTilcbO4.js → server-context-BUX61Gwl.js} +2 -2
  304. package/dist/{server-context-Bbi-nqKD.js → server-context-FPrSnBNG.js} +2 -2
  305. package/dist/{server-lifecycle-DrAGY_SC.js → server-lifecycle-7QoL5fyb.js} +1 -1
  306. package/dist/{server-lifecycle-BIOS9ErD.js → server-lifecycle-DXRap2AR.js} +1 -1
  307. package/dist/{server-node-events-B3c2ctcO.js → server-node-events-DSTI4JnV.js} +18 -18
  308. package/dist/{server-node-events-lwnuZDvV.js → server-node-events-MOX6FUiJ.js} +18 -18
  309. package/dist/{service-Hy5BSgmp.js → service-C_LTJsJU.js} +1 -1
  310. package/dist/{service-RnhuB5cG.js → service-Dkcqg-rd.js} +1 -1
  311. package/dist/{session-meta-DH__PMKt.js → session-meta-DuK_BrWT.js} +2 -2
  312. package/dist/{sessions-DCXadUdD.js → sessions-CgVN798h.js} +4 -4
  313. package/dist/{sessions-Cy_VVMEN.js → sessions-Dcu0bdQ7.js} +4 -4
  314. package/dist/{shared-Bc-7t0_-.js → shared-6Uo_6p6X.js} +1 -1
  315. package/dist/{shared-CJd0QML9.js → shared-Cj18nzyW.js} +1 -1
  316. package/dist/{skill-commands-CRZajtK_.js → skill-commands-BhMQp_i8.js} +9 -9
  317. package/dist/{skills-DOc6uEd1.js → skills-ckuLxkyg.js} +21 -21
  318. package/dist/{status-PwhjcAYw.js → status-B3eNMcE3.js} +7 -7
  319. package/dist/{status-Dh0qVJb_.js → status-C5pbfqhh.js} +7 -7
  320. package/dist/{store-xxN6qyAh.js → store-gQ6FSjVk.js} +2 -2
  321. package/dist/{subsystem-BCjklMkJ.js → subsystem-CPYnUTN4.js} +64 -64
  322. package/dist/{systemd-B5tdOLBg.js → systemd-BJqlwupF.js} +1 -1
  323. package/dist/{systemd-DsBMxUv1.js → systemd-D6tSdsgx.js} +1 -1
  324. package/dist/{systemd-hints-DH86kn6U.js → systemd-hints-Cp0IfOPD.js} +45 -4
  325. package/dist/{systemd-hints-WHzM5Tf8.js → systemd-hints-DzBfweRH.js} +45 -4
  326. package/dist/{systemd-linger-BPzLwumk.js → systemd-linger-Dk1GWduD.js} +1 -1
  327. package/dist/{systemd-linger-EvpJ4ZxF.js → systemd-linger-NvIOU-Ez.js} +1 -1
  328. package/dist/{tables-o3ZpyPWV.js → tables-DEyD0zuy.js} +1 -1
  329. package/dist/{task-pipeline-boot-CcSZVGUe.js → task-pipeline-boot-BLEjtMkt.js} +17 -17
  330. package/dist/{task-pipeline-boot-UcskPjY2.js → task-pipeline-boot-DKqT4VdZ.js} +17 -17
  331. package/dist/{tokens-9Mla_HDp.js → tokens-9BPY4SaX.js} +1 -1
  332. package/dist/{tool-loop-detection-8uvf8mSd.js → tool-loop-detection-C1fqyXXD.js} +2 -2
  333. package/dist/{tui-WSQKUD4o.js → tui-5QhX2Lcm.js} +1 -1
  334. package/dist/{tui-dccCFhet.js → tui-CetFSZIt.js} +1 -1
  335. package/dist/{tui-cli-C3Sopj4_.js → tui-cli-Bo-8qeoa.js} +4 -4
  336. package/dist/{tui-cli-CpRsjtOO.js → tui-cli-DFak7s89.js} +4 -4
  337. package/dist/{update-D86-cS_v.js → update-B3jJ2yVe.js} +1 -1
  338. package/dist/{update-DJaR3w6x.js → update-Dwt-MeRL.js} +1 -1
  339. package/dist/{update-cli-BSxfgTyN.js → update-cli-D5knJbn5.js} +34 -34
  340. package/dist/{update-cli-f5iTpbNn.js → update-cli-Dhxx3iie.js} +33 -33
  341. package/dist/{update-runner-DwM4xj8C.js → update-runner-18yD5NLo.js} +1 -1
  342. package/dist/{update-runner-DqE_9Axt.js → update-runner-DohE1D7D.js} +1 -1
  343. package/dist/{user-run-recording-CAk1Ketk.js → user-run-recording-B0XAAUwL.js} +2 -2
  344. package/dist/{validation-cli-C8XHeLjI.js → validation-cli-CAge8d_F.js} +18 -18
  345. package/dist/{validation-cli-jSmCMkkm.js → validation-cli-Dfac8Duq.js} +18 -18
  346. package/dist/{with-timeout-BvxLQUTs.js → with-timeout-1SSQaGSz.js} +2 -2
  347. package/dist/{with-timeout-CII3PkDO.js → with-timeout-DD5bV7yq.js} +2 -2
  348. package/dist/{workspace-Dbn4gFdH.js → workspace-B707AGIc.js} +9 -9
  349. package/extensions/acpx/package.json +1 -1
  350. package/extensions/anthropic/package.json +1 -1
  351. package/extensions/bifrost/package.json +1 -1
  352. package/extensions/bluebubbles/package.json +1 -1
  353. package/extensions/copilot-proxy/package.json +1 -1
  354. package/extensions/deepgram/package.json +1 -1
  355. package/extensions/deepinfra/package.json +1 -1
  356. package/extensions/diagnostics-otel/package.json +1 -1
  357. package/extensions/diffs/package.json +1 -1
  358. package/extensions/discord/package.json +1 -1
  359. package/extensions/dream-cycle/package.json +1 -1
  360. package/extensions/elevenlabs/package.json +1 -1
  361. package/extensions/feishu/package.json +1 -1
  362. package/extensions/gemini-search/package.json +1 -1
  363. package/extensions/google/package.json +1 -1
  364. package/extensions/google-gemini-cli-auth/package.json +1 -1
  365. package/extensions/googlechat/package.json +1 -1
  366. package/extensions/grok-search/package.json +1 -1
  367. package/extensions/groq/package.json +1 -1
  368. package/extensions/imessage/package.json +1 -1
  369. package/extensions/irc/package.json +1 -1
  370. package/extensions/kimi-search/package.json +1 -1
  371. package/extensions/learning-core/package.json +1 -1
  372. package/extensions/learning-core/src/bounded-store-reads.conformance.test.ts +23 -411
  373. package/extensions/learning-core/src/bounded-store-reads.pg.integration.test.ts +52 -0
  374. package/extensions/learning-core/src/bounded-store-reads.test-helpers.ts +387 -0
  375. package/extensions/learning-core/src/lesson-store-conformance.pg.integration.test.ts +77 -0
  376. package/extensions/learning-core/src/lesson-store-conformance.test-helpers.ts +1406 -0
  377. package/extensions/learning-core/src/lesson-store-conformance.test.ts +27 -1509
  378. package/extensions/learning-core/src/lesson-store-postgres.test.ts +9 -7
  379. package/extensions/line/package.json +1 -1
  380. package/extensions/llm-task/package.json +1 -1
  381. package/extensions/lobster/package.json +1 -1
  382. package/extensions/matrix/CHANGELOG.md +1 -1
  383. package/extensions/matrix/package.json +1 -1
  384. package/extensions/mattermost/package.json +1 -1
  385. package/extensions/memory-core/package.json +1 -1
  386. package/extensions/memory-lancedb/package.json +1 -1
  387. package/extensions/minimax/package.json +1 -1
  388. package/extensions/minimax-portal-auth/package.json +1 -1
  389. package/extensions/mistral/package.json +1 -1
  390. package/extensions/moonshot/package.json +1 -1
  391. package/extensions/msteams/CHANGELOG.md +1 -1
  392. package/extensions/msteams/package.json +1 -1
  393. package/extensions/nextcloud-talk/package.json +1 -1
  394. package/extensions/nostr/CHANGELOG.md +1 -1
  395. package/extensions/nostr/package.json +1 -1
  396. package/extensions/open-prose/package.json +1 -1
  397. package/extensions/openai/package.json +1 -1
  398. package/extensions/openai-realtime/package.json +1 -1
  399. package/extensions/openrouter/package.json +1 -1
  400. package/extensions/perplexity-search/package.json +1 -1
  401. package/extensions/searxng-search/package.json +1 -1
  402. package/extensions/signal/package.json +1 -1
  403. package/extensions/slack/package.json +1 -1
  404. package/extensions/synology-chat/package.json +1 -1
  405. package/extensions/telegram/package.json +1 -1
  406. package/extensions/tlon/package.json +1 -1
  407. package/extensions/twitch/CHANGELOG.md +1 -1
  408. package/extensions/twitch/package.json +1 -1
  409. package/extensions/voice-call/CHANGELOG.md +1 -1
  410. package/extensions/voice-call/package.json +1 -1
  411. package/extensions/whatsapp/package.json +1 -1
  412. package/extensions/zai/package.json +1 -1
  413. package/extensions/zalo/CHANGELOG.md +1 -1
  414. package/extensions/zalo/package.json +1 -1
  415. package/extensions/zalouser/CHANGELOG.md +1 -1
  416. package/extensions/zalouser/package.json +1 -1
  417. package/package.json +1 -1
  418. package/dist/plugin-sdk/agent-turn-execute-BCE3FS2s.js +0 -61
  419. package/dist/plugin-sdk/agent-turn-runner-gf0ARmuP.js +0 -11
  420. package/dist/plugin-sdk/audio-preflight-DlGohKMs.js +0 -28
  421. package/dist/plugin-sdk/deliver-DBCz1GqU.js +0 -32
  422. package/dist/plugin-sdk/manager-ByaW_O4Y.js +0 -16
  423. package/dist/plugin-sdk/markdown-tables-J4hZ7eWU.js +0 -10
  424. package/dist/plugin-sdk/provider-dispatcher-Byy-KtUn.js +0 -61
  425. package/dist/plugin-sdk/provider-image-B_dJDOl5.js +0 -22
  426. package/dist/plugin-sdk/proxy-DwbYzxBy.js +0 -7
  427. package/dist/plugin-sdk/replies-B9uDLFOE.js +0 -25
  428. package/dist/plugin-sdk/reply-prefix-BQMEQam1.js +0 -7
  429. package/dist/plugin-sdk/runs-BQhYekhr.js +0 -25
  430. package/dist/plugin-sdk/send-CeWqZTK0.js +0 -23
  431. package/dist/plugin-sdk/send-CzcKgBBf.js +0 -25
  432. package/dist/plugin-sdk/send-Dvna5ey-.js +0 -27
  433. package/dist/plugin-sdk/send-xPV0QIjh.js +0 -24
  434. package/dist/plugin-sdk/session-meta-n4SCcAbJ.js +0 -10
  435. package/dist/plugin-sdk/skill-commands-BOV3NIbU.js +0 -14
  436. package/dist/plugin-sdk/subagent-registry-CZBK6615.js +0 -61
  437. package/dist/plugin-sdk/user-run-recording-DiKcNdjn.js +0 -11
@@ -3,84 +3,30 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
7
- import { isLearningDomainError } from "./errors.js";
8
- import { withCandidateDefaults } from "./extraction-candidate.js";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
9
7
  import { createLearningSchemaSql } from "./learning-schema.js";
10
- import { withLessonDefaults } from "./lesson-record.js";
11
8
  import {
12
- createDefaultLearningPgPool,
13
- PostgresLessonStore,
14
- type PgPoolLike,
15
- } from "./lesson-store-postgres.js";
9
+ assertScopeIsolation,
10
+ registerLessonStoreConformance,
11
+ TEST_SCHEMA,
12
+ type Backend,
13
+ type MakeStore,
14
+ type StoreScope,
15
+ } from "./lesson-store-conformance.test-helpers.js";
16
+ import { PostgresLessonStore } from "./lesson-store-postgres.js";
16
17
  import { createInMemoryPgPool, type InMemoryPgPool } from "./lesson-store-postgres.test-helpers.js";
17
18
  import { SqliteLessonStore } from "./lesson-store-sqlite.js";
18
19
  import { FileLearningStore } from "./store.js";
19
- import type { ExtractionCandidate, LearningLesson, LessonStore, SkillProposal } from "./types.js";
20
-
21
- type StoreScope = { workspaceId?: string; agentId?: string };
22
- type MakeStore = (scope: StoreScope) => Promise<LessonStore>;
23
-
24
- type Backend = {
25
- // The backend identity describeBackend() must report. Two entries may share it: the postgres
26
- // store is run against both a live server and the in-memory double.
27
- name: "fs-jsonl" | "sqlite" | "postgres";
28
- // How this leg is named in the suite title and the skip report.
29
- label: string;
30
- available: boolean;
31
- make: MakeStore;
32
- // Raw backing rows (or log bytes), for byte-identity assertions that bypass the store.
33
- snapshot: () => Promise<string>;
34
- skipReason?: string;
35
- };
36
-
37
- // Every workspaceId this suite writes carries the run id, so two concurrent runs against one
38
- // shared OPENCLAW_DB_URL can neither read nor delete each other's rows
39
- // (mirrors primary-mode-postgres.e2e.test.ts:21).
40
- // A pid is unique only among LIVE processes on ONE host: two CI shards, two machines against one
41
- // shared server, or a later run after pid reuse all collide -- and this suite DELETEs by prefix
42
- // and DROPs its schema, so a collision is destructive, not merely flaky.
43
- const RUN_TOKEN = crypto.randomUUID().replaceAll("-", "");
44
- const RUN_ID = `test_${RUN_TOKEN}`;
45
- function ws(name: string): string {
46
- return `${RUN_ID}_${name}`;
47
- }
48
-
49
- // `schema` is REQUIRED on PostgresLessonStore; a run-unique name keeps concurrent runs apart.
50
- const TEST_SCHEMA = `plugin_learning_core_test_${RUN_TOKEN}`;
51
- const PG_TABLE = `${TEST_SCHEMA}.learning_records`;
52
-
53
- const EXPECTED_CAPABILITY: Record<Backend["name"], "full" | "reduced"> = {
54
- "fs-jsonl": "reduced",
55
- sqlite: "full",
56
- postgres: "full",
57
- };
20
+ import type { LearningLesson } from "./types.js";
58
21
 
59
22
  // Shared backing resources: reset in beforeEach, torn down in afterEach. ONE per test, so a
60
23
  // make(scope) call returns a scoped view over storage the other scopes also see.
61
24
  let shared: { logDir: string; db: DatabaseSync; memPool: InMemoryPgPool };
62
25
 
63
- type ClosablePool = PgPoolLike & { end?: () => Promise<void> };
64
- let sharedPool: ClosablePool | undefined;
65
-
66
- async function cleanupPostgresRows(): Promise<void> {
67
- if (!sharedPool) {
68
- return;
69
- }
70
- // The table only exists once the postgres leg has run its DDL; probing avoids swallowing a
71
- // real error in a bare catch.
72
- const probe = await sharedPool.query("SELECT to_regclass($1) AS reg", [PG_TABLE]);
73
- if (!probe.rows[0]?.reg) {
74
- return;
75
- }
76
- await sharedPool.query(`DELETE FROM ${PG_TABLE} WHERE workspace_id LIKE $1`, [`${RUN_ID}%`]);
77
- }
78
-
79
26
  const BACKENDS: Backend[] = [
80
27
  {
81
28
  name: "fs-jsonl",
82
29
  label: "fs-jsonl",
83
- available: true,
84
30
  make: async (scope) => new FileLearningStore({ logDir: shared.logDir, scope }),
85
31
  snapshot: async () => {
86
32
  const names = (await fs.readdir(shared.logDir, { recursive: true })).toSorted();
@@ -93,11 +39,13 @@ const BACKENDS: Backend[] = [
93
39
  }
94
40
  return parts.join("\n--\n");
95
41
  },
42
+ assertSearchIndex: async (store) => {
43
+ expect(store.describeBackend().capability).toBe("reduced");
44
+ },
96
45
  },
97
46
  {
98
47
  name: "sqlite",
99
48
  label: "sqlite",
100
- available: true,
101
49
  make: async (scope) => new SqliteLessonStore({ db: shared.db, scope }),
102
50
  snapshot: async () =>
103
51
  JSON.stringify(
@@ -107,48 +55,32 @@ const BACKENDS: Backend[] = [
107
55
  )
108
56
  .all(),
109
57
  ),
110
- },
111
- {
112
- name: "postgres",
113
- label: "postgres (live server)",
114
- available: Boolean(process.env.OPENCLAW_DB_URL),
115
- skipReason: "OPENCLAW_DB_URL not set",
116
- make: async (scope) => {
117
- if (!sharedPool) {
118
- throw new Error("postgres backend requested without OPENCLAW_DB_URL");
119
- }
120
- return new PostgresLessonStore({ pool: sharedPool, schema: TEST_SCHEMA, scope });
121
- },
122
- snapshot: async () => {
123
- const result = await sharedPool!.query(
124
- `SELECT * FROM ${PG_TABLE} WHERE workspace_id LIKE $1
125
- ORDER BY workspace_id, agent_id, record_type, record_id`,
126
- [`${RUN_ID}%`],
127
- );
128
- return JSON.stringify(result.rows);
58
+ assertSearchIndex: async () => {
59
+ const names = shared.db.prepare("SELECT name FROM sqlite_master").all() as Array<{
60
+ name: string;
61
+ }>;
62
+ expect(names.map((row) => row.name)).toContain("learning_lessons_fts");
129
63
  },
130
64
  },
131
65
  {
132
66
  // The postgres store's own logic — row mapping, transaction ordering, merge, chain walking —
133
67
  // is identical whichever server answers, so it is held to the same contract on every host
134
- // rather than only where a database happens to be reachable. The live leg above stays the
135
- // authority on real dialect behaviour; this leg never substitutes for it.
68
+ // rather than only where a database happens to be reachable. The live leg
69
+ // (lesson-store-conformance.pg.integration.test.ts, `pnpm test:pg`) stays the authority on
70
+ // real dialect behaviour; this leg never substitutes for it.
136
71
  name: "postgres",
137
72
  label: "postgres (in-memory double)",
138
- available: true,
139
73
  make: async (scope) =>
140
74
  new PostgresLessonStore({ pool: shared.memPool, schema: TEST_SCHEMA, scope }),
141
75
  snapshot: async () => JSON.stringify(shared.memPool.rows()),
76
+ assertSearchIndex: async () => {
77
+ const sql = createLearningSchemaSql("postgres", { schema: TEST_SCHEMA }).join("\n");
78
+ expect(sql).toMatch(/search_tsv/i);
79
+ expect(sql).toMatch(/\bGIN\b/i);
80
+ },
142
81
  },
143
82
  ];
144
83
 
145
- beforeAll(() => {
146
- const dsn = process.env.OPENCLAW_DB_URL;
147
- if (dsn) {
148
- sharedPool = createDefaultLearningPgPool(dsn);
149
- }
150
- });
151
-
152
84
  beforeEach(async () => {
153
85
  shared = {
154
86
  logDir: await fs.mkdtemp(path.join(os.tmpdir(), "learning-conformance-")),
@@ -160,1428 +92,14 @@ beforeEach(async () => {
160
92
  afterEach(async () => {
161
93
  shared.db.close();
162
94
  await fs.rm(shared.logDir, { recursive: true, force: true });
163
- await cleanupPostgresRows();
164
- });
165
-
166
- afterAll(async () => {
167
- if (sharedPool) {
168
- await sharedPool.query(`DROP SCHEMA IF EXISTS ${TEST_SCHEMA} CASCADE`);
169
- await sharedPool.end?.();
170
- }
171
95
  });
172
96
 
173
- // --- Assertion helpers: written ONCE against LessonStore, run for every backend ---
174
-
175
- async function assertRoundTripsLesson(make: MakeStore) {
176
- const scope = { agentId: "a1", workspaceId: ws("roundtrip") };
177
- const store = await make(scope);
178
-
179
- const lesson: LearningLesson = withLessonDefaults(
180
- {
181
- id: "lesson-roundtrip",
182
- scope,
183
- kind: "procedure",
184
- text: "Do X when Y happens.",
185
- confidence: 0.9,
186
- occurrences: 1,
187
- distinctSessionIds: ["session-1"],
188
- evidence: [
189
- {
190
- sessionId: "session-1",
191
- observedAt: "2026-05-24T10:00:00.000Z",
192
- summary: "Observed in production.",
193
- },
194
- ],
195
- createdAt: "2026-05-24T10:00:00.000Z",
196
- updatedAt: "2026-05-24T10:00:00.000Z",
197
- },
198
- "2026-05-24T10:00:00.000Z",
199
- );
200
-
201
- await store.upsertLesson(lesson);
202
- const retrieved = await store.getLesson("lesson-roundtrip");
203
-
204
- expect(retrieved, "round-trip must preserve every lifecycle field").toEqual(lesson);
205
- }
206
-
207
- // The legacy read-through seeds with this: a row another writer already holds (with its distill
208
- // flags or its invalid_at) must never be overwritten by a stale legacy copy.
209
- async function assertInsertLessonIfAbsentNeverOverwrites(make: MakeStore) {
210
- const scope = { agentId: "a1", workspaceId: ws("insert-if-absent") };
211
- const store = await make(scope);
212
- const at = "2026-05-24T10:00:00.000Z";
213
- const lesson: LearningLesson = withLessonDefaults(
214
- {
215
- id: "lesson-seed",
216
- scope,
217
- kind: "procedure",
218
- text: "Seed me.",
219
- confidence: 0.9,
220
- occurrences: 1,
221
- distinctSessionIds: ["session-1"],
222
- evidence: [],
223
- createdAt: at,
224
- updatedAt: at,
225
- },
226
- at,
227
- );
228
- await store.insertLessonIfAbsent(lesson);
229
- expect(await store.getLesson("lesson-seed")).toEqual(lesson);
230
- const live = { ...lesson, proposed: true, invalidAt: "2026-05-25T00:00:00.000Z" };
231
- await store.upsertLesson(live);
232
- await store.insertLessonIfAbsent(lesson);
233
- expect(await store.getLesson("lesson-seed")).toEqual(live);
234
- }
235
-
236
- async function assertUpsertIsIdempotent(make: MakeStore) {
237
- const scope = { agentId: "a2", workspaceId: ws("idempotent") };
238
- const store = await make(scope);
239
-
240
- const lesson: LearningLesson = withLessonDefaults(
241
- {
242
- id: "lesson-idempotent",
243
- scope,
244
- kind: "failure",
245
- text: "Handle error code E_NOENT.",
246
- confidence: 0.8,
247
- occurrences: 1,
248
- distinctSessionIds: ["session-2"],
249
- evidence: [
250
- {
251
- sessionId: "session-2",
252
- observedAt: "2026-05-24T11:00:00.000Z",
253
- summary: "Error trace logged.",
254
- },
255
- ],
256
- createdAt: "2026-05-24T11:00:00.000Z",
257
- updatedAt: "2026-05-24T11:00:00.000Z",
258
- },
259
- "2026-05-24T11:00:00.000Z",
260
- );
261
-
262
- await store.upsertLesson(lesson);
263
- await store.upsertLesson(lesson);
264
-
265
- const all = await store.listLessons();
266
- expect(all, "a second upsert of the same id must not duplicate the row").toHaveLength(1);
267
- expect(all[0]).toEqual(lesson);
268
- }
269
-
270
- async function assertScopeIsolation(make: MakeStore) {
271
- const scopeA = { agentId: "a1", workspaceId: ws("w1") };
272
- const scopeB = { agentId: "a2", workspaceId: ws("w2") };
273
- const storeW1A1 = await make(scopeA);
274
- const storeW2A2 = await make(scopeB);
275
-
276
- const lessonW1A1: LearningLesson = withLessonDefaults(
277
- {
278
- id: "lesson-scope-1",
279
- scope: scopeA,
280
- kind: "procedure",
281
- text: "Scoped lesson 1.",
282
- confidence: 0.7,
283
- occurrences: 1,
284
- distinctSessionIds: ["sess-1"],
285
- evidence: [
286
- {
287
- sessionId: "sess-1",
288
- observedAt: "2026-05-24T12:00:00.000Z",
289
- summary: "In w1/a1.",
290
- },
291
- ],
292
- createdAt: "2026-05-24T12:00:00.000Z",
293
- updatedAt: "2026-05-24T12:00:00.000Z",
294
- },
295
- "2026-05-24T12:00:00.000Z",
296
- );
297
-
298
- await storeW1A1.upsertLesson(lessonW1A1);
299
-
300
- const visibleToW2A2 = await storeW2A2.listLessons();
301
- const found = visibleToW2A2.find((l) => l.id === lessonW1A1.id);
302
- // The message names the offending scope so the mutation self-check can match on it instead of
303
- // on whatever object vitest happens to dump.
304
- expect(
305
- found,
306
- `scope isolation violated: w2 (${scopeB.workspaceId}/${scopeB.agentId}) can read ${lessonW1A1.id}, written under ${scopeA.workspaceId}/${scopeA.agentId}`,
307
- ).toBeUndefined();
308
- }
309
-
310
- // assertScopeIsolation varies workspace AND agent together, so a store that filters on only ONE of
311
- // the two columns still passes it. This holds each dimension constant in turn, so dropping either
312
- // predicate is caught. (Found by mutating PostgresLessonStore.scopeFilter to skip agent_id: the
313
- // combined-scope assertion above stayed green.)
314
- async function assertScopeIsolationPerDimension(make: MakeStore) {
315
- const workspaceId = ws("dim");
316
-
317
- function lessonAt(id: string, scope: StoreScope): LearningLesson {
318
- return withLessonDefaults(
319
- {
320
- id,
321
- scope,
322
- kind: "procedure",
323
- text: `Lesson ${id}.`,
324
- confidence: 0.7,
325
- occurrences: 1,
326
- distinctSessionIds: ["sess-dim"],
327
- evidence: [
328
- { sessionId: "sess-dim", observedAt: "2026-05-24T12:00:00.000Z", summary: "dim." },
329
- ],
330
- createdAt: "2026-05-24T12:00:00.000Z",
331
- updatedAt: "2026-05-24T12:00:00.000Z",
332
- },
333
- "2026-05-24T12:00:00.000Z",
334
- );
335
- }
336
-
337
- // Same workspace, different agent — only the agent_id predicate can separate these.
338
- const agentA = { workspaceId, agentId: "dim-a1" };
339
- const agentB = { workspaceId, agentId: "dim-a2" };
340
- await (await make(agentA)).upsertLesson(lessonAt("lesson-dim-agent", agentA));
341
- const seenByOtherAgent = (await (await make(agentB)).listLessons()).find(
342
- (l) => l.id === "lesson-dim-agent",
343
- );
344
- expect(
345
- seenByOtherAgent,
346
- `agent isolation violated: agent ${agentB.agentId} can read a lesson written by ${agentA.agentId} in the SAME workspace ${workspaceId}`,
347
- ).toBeUndefined();
348
-
349
- // Same agent, different workspace — only the workspace_id predicate can separate these.
350
- const wsA = { workspaceId: ws("dim-w1"), agentId: "dim-shared" };
351
- const wsB = { workspaceId: ws("dim-w2"), agentId: "dim-shared" };
352
- await (await make(wsA)).upsertLesson(lessonAt("lesson-dim-workspace", wsA));
353
- const seenByOtherWorkspace = (await (await make(wsB)).listLessons()).find(
354
- (l) => l.id === "lesson-dim-workspace",
355
- );
356
- expect(
357
- seenByOtherWorkspace,
358
- `workspace isolation violated: workspace ${wsB.workspaceId} can read a lesson written in ${wsA.workspaceId} by the SAME agent ${wsA.agentId}`,
359
- ).toBeUndefined();
360
- }
361
-
362
- async function assertEmptyScopeReadsAcrossScopes(make: MakeStore) {
363
- const scopeA = { agentId: "a1", workspaceId: ws("cross") };
364
- const storeW1A1 = await make(scopeA);
365
- const storeEmptyScope = await make({});
366
-
367
- const lessonW1A1: LearningLesson = withLessonDefaults(
368
- {
369
- id: "lesson-cross-scope",
370
- scope: scopeA,
371
- kind: "procedure",
372
- text: "Lesson visible across scopes.",
373
- confidence: 0.75,
374
- occurrences: 1,
375
- distinctSessionIds: ["sess-2"],
376
- evidence: [
377
- {
378
- sessionId: "sess-2",
379
- observedAt: "2026-05-24T13:00:00.000Z",
380
- summary: "Cross-scope test.",
381
- },
382
- ],
383
- createdAt: "2026-05-24T13:00:00.000Z",
384
- updatedAt: "2026-05-24T13:00:00.000Z",
385
- },
386
- "2026-05-24T13:00:00.000Z",
387
- );
388
-
389
- await storeW1A1.upsertLesson(lessonW1A1);
390
-
391
- const allLessons = await storeEmptyScope.listLessons();
392
- expect(
393
- allLessons.find((l) => l.id === lessonW1A1.id),
394
- "an empty scope must read across every scope",
395
- ).toBeDefined();
396
- }
397
-
398
- async function assertSupersedeRetainsPredecessor(make: MakeStore) {
399
- const scope = { agentId: "a3", workspaceId: ws("supersede") };
400
- const store = await make(scope);
401
-
402
- const predecessor: LearningLesson = withLessonDefaults(
403
- {
404
- id: "lesson-v1",
405
- scope,
406
- kind: "procedure",
407
- text: "Old approach.",
408
- confidence: 0.6,
409
- occurrences: 1,
410
- distinctSessionIds: ["sess-3"],
411
- evidence: [
412
- {
413
- sessionId: "sess-3",
414
- observedAt: "2026-05-24T14:00:00.000Z",
415
- summary: "Original approach.",
416
- },
417
- ],
418
- createdAt: "2026-05-24T14:00:00.000Z",
419
- updatedAt: "2026-05-24T14:00:00.000Z",
420
- },
421
- "2026-05-24T14:00:00.000Z",
422
- );
423
-
424
- await store.upsertLesson(predecessor);
425
-
426
- const successor: LearningLesson = withLessonDefaults(
427
- {
428
- id: "lesson-v2",
429
- scope,
430
- kind: "procedure",
431
- text: "New improved approach.",
432
- confidence: 0.9,
433
- occurrences: 1,
434
- distinctSessionIds: ["sess-3"],
435
- evidence: [
436
- {
437
- sessionId: "sess-3",
438
- observedAt: "2026-05-24T14:15:00.000Z",
439
- summary: "Improved version.",
440
- },
441
- ],
442
- createdAt: "2026-05-24T14:15:00.000Z",
443
- updatedAt: "2026-05-24T14:15:00.000Z",
444
- },
445
- "2026-05-24T14:15:00.000Z",
446
- );
447
-
448
- const result = await store.supersedeLesson({
449
- predecessorId: "lesson-v1",
450
- successor,
451
- now: "2026-05-24T14:15:00.000Z",
452
- });
453
-
454
- expect(result.supersedesId).toBe("lesson-v1");
455
-
456
- const pred = await store.getLesson("lesson-v1");
457
- expect(pred, "supersede must retain the predecessor").toBeDefined();
458
- expect(pred?.invalidAt, "the predecessor must carry invalid_at").toBe("2026-05-24T14:15:00.000Z");
459
-
460
- const succ = await store.getLesson("lesson-v2");
461
- expect(succ?.supersedesId, "the successor must link back to the predecessor").toBe("lesson-v1");
462
- }
463
-
464
- // A successor sharing the predecessor's id would overwrite the same primary key with a row whose
465
- // supersedesId points at itself, and every later getLessonChain would throw "cycle detected" -- an
466
- // unrecoverable break in the provenance guarantee. The tool derives the successor id by hashing the
467
- // text, so identical text reaches this path from a real operator action, not just a bad caller.
468
- // The ordering contract. Backends store records in different physical layouts (append-only log
469
- // position, SQLite rowid, a Postgres heap row), and an UPDATE perturbs each layout differently:
470
- // `INSERT OR REPLACE` mints a new rowid, the log's dedup keeps the newest globalSeq, while a real
471
- // `ON CONFLICT DO UPDATE` leaves the row where it was. Ordering therefore has to be stated on the
472
- // data (createdAt, id tiebreak), not inherited from storage. gateway-methods.ts hands list output
473
- // straight to the operator with no sort, so a divergence here is directly user-visible.
474
- //
475
- // The update must target the EARLIER record, after the later one exists. Updating the most recent
476
- // record leaves it last under every layout, so the assertion would pass on all three backends while
477
- // the divergence was fully present.
478
- async function assertUpdateDoesNotReorder(make: MakeStore) {
479
- const scope = { agentId: "a4order", workspaceId: ws("order") };
480
- const store = await make(scope);
481
- const early = "2026-05-24T09:00:00.000Z";
482
- const late = "2026-05-24T17:00:00.000Z";
483
-
484
- function at(
485
- id: string,
486
- createdAt: string,
487
- text: string,
488
- evidence: LearningLesson["evidence"] = [],
489
- ): LearningLesson {
490
- return withLessonDefaults(
491
- {
492
- id,
493
- scope,
494
- kind: "procedure",
495
- text,
496
- confidence: 0.7,
497
- occurrences: 1,
498
- distinctSessionIds: ["sess-order"],
499
- evidence,
500
- createdAt,
501
- updatedAt: createdAt,
502
- },
503
- createdAt,
504
- );
505
- }
506
-
507
- await store.upsertLesson(at("lesson-early", early, "first"));
508
- await store.upsertLesson(at("lesson-late", late, "second"));
509
- expect((await store.listLessons()).map((l) => l.id)).toEqual(["lesson-early", "lesson-late"]);
510
-
511
- // The merge must genuinely MUTATE the record. mergeLessons ignores candidate.text and derives
512
- // every change from candidate.evidence, so a candidate with no new evidence is a no-op: the
513
- // fs-jsonl store emits no delta, appends nothing, and the list cannot reorder — the assertion
514
- // below would then pass on every backend with the sort removed. Supplying unseen evidence is what
515
- // makes this leg able to fail.
516
- await store.mergeLesson(
517
- at("lesson-early", early, "first", [
518
- { sessionId: "sess-order-2", observedAt: late, summary: "a later observation" },
519
- ]),
520
- );
521
-
522
- expect(
523
- (await store.listLessons()).map((l) => l.id),
524
- "updating the EARLIER lesson must not move it in the list",
525
- ).toEqual(["lesson-early", "lesson-late"]);
526
- }
527
-
528
- async function assertSupersedeRejectsSelfReference(make: MakeStore) {
529
- const scope = { agentId: "a3self", workspaceId: ws("supersede-self") };
530
- const store = await make(scope);
531
- const now = "2026-05-24T14:15:00.000Z";
532
-
533
- await store.upsertLesson(
534
- withLessonDefaults(
535
- {
536
- id: "lesson-self",
537
- scope,
538
- kind: "correction",
539
- text: "original",
540
- confidence: 0.9,
541
- occurrences: 1,
542
- distinctSessionIds: ["sess-self"],
543
- evidence: [],
544
- createdAt: now,
545
- updatedAt: now,
546
- },
547
- now,
548
- ),
549
- );
550
-
551
- await expect(
552
- store.supersedeLesson({
553
- predecessorId: "lesson-self",
554
- successor: withLessonDefaults(
555
- {
556
- id: "lesson-self",
557
- scope,
558
- kind: "correction",
559
- text: "original",
560
- confidence: 0.9,
561
- occurrences: 1,
562
- distinctSessionIds: ["sess-self"],
563
- evidence: [],
564
- createdAt: now,
565
- updatedAt: now,
566
- },
567
- now,
568
- ),
569
- now,
570
- }),
571
- "a successor sharing the predecessor id must be refused, not written",
572
- ).rejects.toThrow(/must differ from the predecessor/);
573
-
574
- // The refusal must leave the predecessor untouched -- a partial write is the corruption itself.
575
- const pred = await store.getLesson("lesson-self");
576
- expect(
577
- pred?.invalidAt,
578
- "the refused supersede must not have closed the predecessor",
579
- ).toBeUndefined();
580
- expect(
581
- pred?.supersedesId,
582
- "the refused supersede must not have self-linked the record",
583
- ).toBeUndefined();
584
- await expect(
585
- store.getLessonChain("lesson-self"),
586
- "the chain must still be readable after the refusal",
587
- ).resolves.toBeDefined();
588
- }
589
-
590
- async function assertActiveListExcludesInvalidated(make: MakeStore) {
591
- const scope = { agentId: "a4", workspaceId: ws("active") };
592
- const store = await make(scope);
593
-
594
- const lesson: LearningLesson = withLessonDefaults(
595
- {
596
- id: "lesson-active",
597
- scope,
598
- kind: "preference",
599
- text: "Active lesson.",
600
- confidence: 0.8,
601
- occurrences: 1,
602
- distinctSessionIds: ["sess-4"],
603
- evidence: [
604
- {
605
- sessionId: "sess-4",
606
- observedAt: "2026-05-24T15:00:00.000Z",
607
- summary: "Active.",
608
- },
609
- ],
610
- createdAt: "2026-05-24T15:00:00.000Z",
611
- updatedAt: "2026-05-24T15:00:00.000Z",
612
- },
613
- "2026-05-24T15:00:00.000Z",
614
- );
615
-
616
- await store.upsertLesson(lesson);
617
-
618
- const invalidLesson: LearningLesson = withLessonDefaults(
619
- {
620
- id: "lesson-invalid",
621
- scope,
622
- kind: "preference",
623
- text: "Invalidated lesson.",
624
- confidence: 0.5,
625
- occurrences: 1,
626
- distinctSessionIds: ["sess-4"],
627
- evidence: [
628
- {
629
- sessionId: "sess-4",
630
- observedAt: "2026-05-24T15:30:00.000Z",
631
- summary: "Invalid.",
632
- },
633
- ],
634
- createdAt: "2026-05-24T15:30:00.000Z",
635
- updatedAt: "2026-05-24T15:30:00.000Z",
636
- invalidAt: "2026-05-24T16:00:00.000Z",
637
- },
638
- "2026-05-24T15:30:00.000Z",
639
- );
640
-
641
- await store.upsertLesson(invalidLesson);
642
-
643
- expect(await store.getLesson("lesson-active")).toBeDefined();
644
- expect(
645
- await store.getLesson("lesson-invalid"),
646
- "getLesson must still return an invalidated record",
647
- ).toBeDefined();
648
-
649
- // BOTH ends of the window, not just invalidAt: a record whose validFrom has not arrived is not
650
- // active either, and only asserting the expiry end would leave that half uncovered per backend.
651
- const notYetValid: LearningLesson = withLessonDefaults(
652
- {
653
- id: "lesson-not-yet-valid",
654
- scope,
655
- kind: "preference",
656
- text: "Not yet effective.",
657
- confidence: 0.5,
658
- occurrences: 1,
659
- distinctSessionIds: ["sess-4"],
660
- evidence: [],
661
- createdAt: "2026-05-24T15:30:00.000Z",
662
- updatedAt: "2026-05-24T15:30:00.000Z",
663
- validFrom: "2099-01-01T00:00:00.000Z",
664
- },
665
- "2026-05-24T15:30:00.000Z",
666
- );
667
-
668
- await store.upsertLesson(notYetValid);
669
-
670
- const now = "2026-05-24T17:00:00.000Z";
671
- const active = await store.listActiveLessons(now);
672
- expect(active.find((l) => l.id === "lesson-active")).toBeDefined();
673
- expect(
674
- active.find((l) => l.id === "lesson-invalid"),
675
- "listActiveLessons must exclude an invalidated record",
676
- ).toBeUndefined();
677
- expect(
678
- active.find((l) => l.id === "lesson-not-yet-valid"),
679
- "listActiveLessons must exclude a record whose validFrom has not arrived",
680
- ).toBeUndefined();
681
- expect(
682
- await store.getLesson("lesson-not-yet-valid"),
683
- "getLesson must still return a not-yet-valid record",
684
- ).toBeDefined();
685
- }
686
-
687
- async function assertChainWalksNewestFirstAndRejectsCycle(make: MakeStore) {
688
- const scope = { agentId: "a5", workspaceId: ws("chain") };
689
- const store = await make(scope);
690
-
691
- const v1: LearningLesson = withLessonDefaults(
692
- {
693
- id: "lesson-v1-chain",
694
- scope,
695
- kind: "procedure",
696
- text: "Version 1.",
697
- confidence: 0.5,
698
- occurrences: 1,
699
- distinctSessionIds: ["sess-5"],
700
- evidence: [
701
- {
702
- sessionId: "sess-5",
703
- observedAt: "2026-05-24T17:00:00.000Z",
704
- summary: "V1.",
705
- },
706
- ],
707
- createdAt: "2026-05-24T17:00:00.000Z",
708
- updatedAt: "2026-05-24T17:00:00.000Z",
709
- },
710
- "2026-05-24T17:00:00.000Z",
711
- );
712
-
713
- await store.upsertLesson(v1);
714
-
715
- await store.supersedeLesson({
716
- predecessorId: "lesson-v1-chain",
717
- successor: withLessonDefaults(
718
- {
719
- id: "lesson-v2-chain",
720
- scope,
721
- kind: "procedure",
722
- text: "Version 2.",
723
- confidence: 0.7,
724
- occurrences: 1,
725
- distinctSessionIds: ["sess-5"],
726
- evidence: [
727
- {
728
- sessionId: "sess-5",
729
- observedAt: "2026-05-24T17:30:00.000Z",
730
- summary: "V2.",
731
- },
732
- ],
733
- createdAt: "2026-05-24T17:30:00.000Z",
734
- updatedAt: "2026-05-24T17:30:00.000Z",
735
- },
736
- "2026-05-24T17:30:00.000Z",
737
- ),
738
- now: "2026-05-24T17:30:00.000Z",
739
- });
740
-
741
- await store.supersedeLesson({
742
- predecessorId: "lesson-v2-chain",
743
- successor: withLessonDefaults(
744
- {
745
- id: "lesson-v3-chain",
746
- scope,
747
- kind: "procedure",
748
- text: "Version 3.",
749
- confidence: 0.95,
750
- occurrences: 1,
751
- distinctSessionIds: ["sess-5"],
752
- evidence: [
753
- {
754
- sessionId: "sess-5",
755
- observedAt: "2026-05-24T18:00:00.000Z",
756
- summary: "V3.",
757
- },
758
- ],
759
- createdAt: "2026-05-24T18:00:00.000Z",
760
- updatedAt: "2026-05-24T18:00:00.000Z",
761
- },
762
- "2026-05-24T18:00:00.000Z",
763
- ),
764
- now: "2026-05-24T18:00:00.000Z",
765
- });
766
-
767
- const chain = await store.getLessonChain("lesson-v3-chain");
768
- expect(
769
- chain.map((lesson) => lesson.id),
770
- "getLessonChain must walk newest-first",
771
- ).toEqual(["lesson-v3-chain", "lesson-v2-chain", "lesson-v1-chain"]);
772
-
773
- // A corrupt pair of mutual back-links must be rejected, not walked forever. supersedeLesson
774
- // cannot produce this, so the cycle is written through upsertLesson.
775
- const cycleA: LearningLesson = withLessonDefaults(
776
- {
777
- id: "lesson-cycle-a",
778
- scope,
779
- supersedesId: "lesson-cycle-b",
780
- kind: "procedure",
781
- text: "Cycle A.",
782
- confidence: 0.5,
783
- occurrences: 1,
784
- distinctSessionIds: ["sess-5"],
785
- evidence: [
786
- {
787
- sessionId: "sess-5",
788
- observedAt: "2026-05-24T18:30:00.000Z",
789
- summary: "Cycle A.",
790
- },
791
- ],
792
- createdAt: "2026-05-24T18:30:00.000Z",
793
- updatedAt: "2026-05-24T18:30:00.000Z",
794
- },
795
- "2026-05-24T18:30:00.000Z",
796
- );
797
- const cycleB: LearningLesson = withLessonDefaults(
798
- {
799
- id: "lesson-cycle-b",
800
- scope,
801
- supersedesId: "lesson-cycle-a",
802
- kind: "procedure",
803
- text: "Cycle B.",
804
- confidence: 0.5,
805
- occurrences: 1,
806
- distinctSessionIds: ["sess-5"],
807
- evidence: [
808
- {
809
- sessionId: "sess-5",
810
- observedAt: "2026-05-24T18:31:00.000Z",
811
- summary: "Cycle B.",
812
- },
813
- ],
814
- createdAt: "2026-05-24T18:31:00.000Z",
815
- updatedAt: "2026-05-24T18:31:00.000Z",
816
- },
817
- "2026-05-24T18:31:00.000Z",
818
- );
819
-
820
- await store.upsertLesson(cycleA);
821
- await store.upsertLesson(cycleB);
822
-
823
- await expect(store.getLessonChain("lesson-cycle-a")).rejects.toThrow(/cycle/i);
824
- }
825
-
826
- async function assertMergeAccumulatesOccurrencesAndEvidence(make: MakeStore) {
827
- const scope = { agentId: "a7", workspaceId: ws("merge") };
828
- const store = await make(scope);
829
-
830
- const lesson1: LearningLesson = withLessonDefaults(
831
- {
832
- id: "lesson-merge",
833
- scope,
834
- kind: "failure",
835
- text: "Handle timeouts.",
836
- confidence: 0.7,
837
- occurrences: 1,
838
- distinctSessionIds: ["sess-7a"],
839
- evidence: [
840
- {
841
- evidenceId: "ev-1",
842
- sessionId: "sess-7a",
843
- observedAt: "2026-05-24T19:00:00.000Z",
844
- summary: "First occurrence.",
845
- },
846
- ],
847
- createdAt: "2026-05-24T19:00:00.000Z",
848
- updatedAt: "2026-05-24T19:00:00.000Z",
849
- },
850
- "2026-05-24T19:00:00.000Z",
851
- );
852
-
853
- const merged1 = await store.mergeLesson(lesson1);
854
- expect(merged1.occurrences).toBe(1);
855
- expect(merged1.evidence).toHaveLength(1);
856
-
857
- const lesson2: LearningLesson = withLessonDefaults(
858
- {
859
- id: "lesson-merge",
860
- scope,
861
- kind: "failure",
862
- text: "Handle timeouts.",
863
- confidence: 0.85,
864
- occurrences: 1,
865
- distinctSessionIds: ["sess-7b"],
866
- evidence: [
867
- {
868
- evidenceId: "ev-2",
869
- sessionId: "sess-7b",
870
- observedAt: "2026-05-24T19:30:00.000Z",
871
- summary: "Second occurrence.",
872
- },
873
- ],
874
- createdAt: "2026-05-24T19:00:00.000Z",
875
- updatedAt: "2026-05-24T19:30:00.000Z",
876
- },
877
- "2026-05-24T19:00:00.000Z",
878
- );
879
-
880
- const merged2 = await store.mergeLesson(lesson2);
881
- expect(merged2.occurrences, "merge must accumulate occurrences").toBe(2);
882
- expect(merged2.evidence, "merge must accumulate evidence").toHaveLength(2);
883
- expect(merged2.distinctSessionIds).toEqual(["sess-7a", "sess-7b"]);
884
- expect(merged2.confidence).toBe(0.85);
885
- }
886
-
887
- async function assertProposalAndAuditRoundTrip(make: MakeStore) {
888
- const store = await make({ agentId: "a8", workspaceId: ws("proposal") });
889
-
890
- const proposal: SkillProposal = {
891
- id: "proposal-1",
892
- lessonIds: ["lesson-1"],
893
- status: "pending",
894
- skillName: "test-skill",
895
- title: "Test proposal",
896
- body: "# Test",
897
- scannerFindings: [],
898
- createdAt: "2026-05-24T20:00:00.000Z",
899
- updatedAt: "2026-05-24T20:00:00.000Z",
900
- };
901
-
902
- await store.insertProposalIfAbsent(proposal);
903
- const retrieved = await store.getProposal("proposal-1");
904
- expect(retrieved).toEqual(proposal);
905
-
906
- const decided = await store.recordProposalDecision({
907
- proposalId: "proposal-1",
908
- status: "applied",
909
- actor: "test-user",
910
- now: "2026-05-24T20:15:00.000Z",
911
- });
912
-
913
- expect(decided.status).toBe("applied");
914
- expect(decided.decidedBy).toBeDefined();
915
- expect(decided.decidedAt).toBe("2026-05-24T20:15:00.000Z");
916
-
917
- const audit = await store.listAudit();
918
- expect(audit.length, "the decision must append an audit event").toBeGreaterThan(0);
919
- }
920
-
921
- async function assertFindProposalWithScope(make: MakeStore) {
922
- const scope = { agentId: "a9", workspaceId: ws("proposal-scope") };
923
- const store = await make(scope);
924
-
925
- const proposal: SkillProposal = {
926
- id: "proposal-scoped",
927
- lessonIds: ["lesson-1"],
928
- status: "pending",
929
- skillName: "test-skill",
930
- title: "Test proposal",
931
- body: "# Test",
932
- scannerFindings: [],
933
- createdAt: "2026-05-24T20:00:00.000Z",
934
- updatedAt: "2026-05-24T20:00:00.000Z",
935
- };
936
- await store.insertProposalIfAbsent(proposal);
937
-
938
- // Look it up through a store built with an EMPTY scope. That is exactly how the gateway calls it
939
- // (index.ts wires `createLessonStore({ ..., scope: {} })`), and it is the leg that fails today.
940
- const crossScope = await make({});
941
- const found = await crossScope.findProposalWithScope("proposal-scoped");
942
- expect(
943
- found,
944
- "a proposal written under one scope must be findable from an empty scope",
945
- ).toBeDefined();
946
- expect(found?.proposal.id).toBe("proposal-scoped");
947
- // The proposal's OWN scope must come back -- NOT the caller's. If this returns the empty scope,
948
- // decide() resolves no workspace and apply dies with "workspace unresolvable; cannot apply".
949
- expect(found?.scope.workspaceId).toBe(scope.workspaceId);
950
- expect(found?.scope.agentId).toBe(scope.agentId);
951
-
952
- // A proposal written WITHOUT a scope must read back as an empty scope on EVERY backend. The SQL
953
- // stores persist an absent scope field as "" (writeRecord: `this.scope.workspaceId ?? ""`), never
954
- // NULL, so a column read that only guards against NULL hands back {workspaceId: "", agentId: ""}
955
- // and diverges from fs-jsonl, which returns the raw log scope `{}`.
956
- const unscopedStore = await make({});
957
- await unscopedStore.insertProposalIfAbsent({
958
- ...proposal,
959
- id: "proposal-unscoped",
960
- lessonIds: ["lesson-2"],
961
- skillName: "test-skill-unscoped",
962
- });
963
- const unscoped = await crossScope.findProposalWithScope("proposal-unscoped");
964
- expect(unscoped?.proposal.id).toBe("proposal-unscoped");
965
- expect(unscoped?.scope).toEqual({});
966
-
967
- expect(await crossScope.findProposalWithScope("no-such-proposal")).toBeUndefined();
968
- }
969
-
970
- async function assertSupersedeRejectsOccupiedSuccessorId(make: MakeStore) {
971
- // The tool derives the successor id from the replacement TEXT, so text that duplicates another
972
- // lesson's text mints THAT lesson's id. Writing it would overwrite an unrelated record with the
973
- // predecessor's carried fields and still report success -- silent cross-lesson data loss.
974
- const scope = { agentId: "a1", workspaceId: ws("occupied-successor") };
975
- const store = await make(scope);
976
- const at = "2026-05-24T12:00:00.000Z";
977
- const make_ = (id: string): LearningLesson =>
978
- withLessonDefaults(
979
- {
980
- id,
981
- scope,
982
- kind: "procedure",
983
- text: `text for ${id}`,
984
- confidence: 0.5,
985
- occurrences: 1,
986
- distinctSessionIds: [],
987
- evidence: [],
988
- createdAt: at,
989
- updatedAt: at,
990
- },
991
- at,
992
- );
993
- const predecessor = make_("lesson-occupied-pred");
994
- const bystander = make_("lesson-occupied-bystander");
995
- await store.upsertLesson(predecessor);
996
- await store.upsertLesson(bystander);
997
-
998
- await expect(
999
- store.supersedeLesson({
1000
- predecessorId: predecessor.id,
1001
- successor: make_(bystander.id),
1002
- now: "2026-05-24T13:00:00.000Z",
1003
- }),
1004
- "a successor id that already belongs to another lesson must be refused",
1005
- ).rejects.toThrow(/already belongs to another lesson/);
1006
-
1007
- // And nothing may have been written on the way to that refusal.
1008
- expect(await store.getLesson(bystander.id)).toEqual(bystander);
1009
- expect((await store.getLesson(predecessor.id))?.invalidAt).toBeUndefined();
1010
- }
1011
-
1012
- async function assertIdempotentResupersede(make: MakeStore) {
1013
- const scope = { agentId: "a1", workspaceId: ws("resupersede") };
1014
- const store = await make(scope);
1015
- const pred: LearningLesson = withLessonDefaults(
1016
- {
1017
- id: "lesson-pred",
1018
- scope,
1019
- kind: "correction",
1020
- text: "first",
1021
- confidence: 0.9,
1022
- occurrences: 1,
1023
- distinctSessionIds: ["s1"],
1024
- evidence: [],
1025
- createdAt: "2026-08-01T00:00:00.000Z",
1026
- updatedAt: "2026-08-01T00:00:00.000Z",
1027
- },
1028
- "2026-08-01T00:00:00.000Z",
1029
- );
1030
- await store.upsertLesson(pred);
1031
- const makeSucc = (id: string, text: string, at: string): LearningLesson =>
1032
- withLessonDefaults(
1033
- {
1034
- id,
1035
- scope,
1036
- kind: "correction",
1037
- text,
1038
- confidence: 0.9,
1039
- occurrences: 1,
1040
- distinctSessionIds: ["s1"],
1041
- evidence: [],
1042
- createdAt: at,
1043
- updatedAt: at,
1044
- },
1045
- at,
1046
- );
1047
- const first = await store.supersedeLesson({
1048
- predecessorId: pred.id,
1049
- successor: makeSucc("lesson-succ-1", "second", "2026-08-02T00:00:00.000Z"),
1050
- now: "2026-08-02T00:00:00.000Z",
1051
- });
1052
- const second = await store.supersedeLesson({
1053
- predecessorId: pred.id,
1054
- successor: makeSucc("lesson-succ-2", "third", "2026-08-03T00:00:00.000Z"),
1055
- now: "2026-08-03T00:00:00.000Z",
1056
- });
1057
- expect(second.id).toBe(first.id);
1058
- const all = await store.listLessons();
1059
- expect(all.filter((lesson) => lesson.supersedesId === pred.id)).toHaveLength(1);
1060
- }
1061
-
1062
- async function assertCandidateRoundTrip(make: MakeStore) {
1063
- const store = await make({ workspaceId: ws("cand"), agentId: "a1" });
1064
- const candidate = withCandidateDefaults(
1065
- {
1066
- sessionId: "sess-1",
1067
- workspaceId: ws("cand"),
1068
- agentId: "a1",
1069
- excerpt: "redacted",
1070
- tags: ["correction"],
1071
- },
1072
- "2026-08-15T00:00:00.000Z",
1073
- );
1074
- await store.upsertCandidate(candidate);
1075
- await store.upsertCandidate({ ...candidate, excerpt: "redacted-2" });
1076
- const got = await store.getCandidate("sess-1");
1077
- expect(got?.excerpt).toBe("redacted-2");
1078
- expect(got?.status).toBe("pending");
1079
- const pending = await store.listPendingCandidates({ tags: ["correction"] });
1080
- expect(pending.map((entry) => entry.sessionId)).toEqual(["sess-1"]);
1081
- await store.markCandidateProcessed("sess-1", {
1082
- status: "processed",
1083
- promotedLessonId: "lesson-x",
1084
- });
1085
- expect((await store.listPendingCandidates()).map((entry) => entry.sessionId)).not.toContain(
1086
- "sess-1",
1087
- );
1088
- expect((await store.getCandidate("sess-1"))?.promotedLessonId).toBe("lesson-x");
1089
- expect(await store.listLessons()).toHaveLength(0);
1090
- }
1091
-
1092
- async function assertGetCandidatesBySessionIdsRoundTrip(make: MakeStore) {
1093
- const scope = { agentId: "a1", workspaceId: ws("batch-candidates") };
1094
- const store = await make(scope);
1095
- const candidate = {
1096
- sessionId: "sess-a",
1097
- excerpt: "redacted",
1098
- tags: ["correction"],
1099
- status: "pending",
1100
- endedAt: "2026-05-24T20:00:00.000Z",
1101
- } as ExtractionCandidate;
1102
- await store.upsertCandidate(candidate);
1103
- await store.upsertCandidate({ ...candidate, sessionId: "sess-b" });
1104
-
1105
- const found = await store.getCandidatesBySessionIds(["sess-a", "sess-b", "sess-missing"]);
1106
- expect(
1107
- found.map((entry) => entry.sessionId).sort(),
1108
- "found ids only — missing ids are absent, not an error",
1109
- ).toEqual(["sess-a", "sess-b"]);
1110
- expect(await store.getCandidatesBySessionIds([])).toEqual([]);
1111
-
1112
- const otherAgent = await make({ agentId: "a2", workspaceId: ws("batch-candidates") });
1113
- expect(
1114
- await otherAgent.getCandidatesBySessionIds(["sess-a"]),
1115
- "agent isolation holds for the batch read in the same workspace",
1116
- ).toEqual([]);
1117
- const otherWorkspace = await make({ agentId: "a1", workspaceId: ws("batch-other") });
1118
- expect(
1119
- await otherWorkspace.getCandidatesBySessionIds(["sess-a"]),
1120
- "workspace isolation holds for the batch read with the same agent",
1121
- ).toEqual([]);
1122
- }
1123
-
1124
- async function assertSearchLessons(make: MakeStore, backend: Backend) {
1125
- const store = await make({ workspaceId: ws("srch"), agentId: "a1" });
1126
- await store.upsertLesson(
1127
- withLessonDefaults(
1128
- {
1129
- id: "lesson-hit",
1130
- scope: { workspaceId: ws("srch"), agentId: "a1" },
1131
- kind: "procedure",
1132
- text: "Always use pnpm-lock.yaml",
1133
- confidence: 0.9,
1134
- occurrences: 1,
1135
- distinctSessionIds: ["s"],
1136
- evidence: [],
1137
- createdAt: "2026-08-15T00:00:00.000Z",
1138
- updatedAt: "2026-08-15T00:00:00.000Z",
1139
- },
1140
- "2026-08-15T00:00:00.000Z",
1141
- ),
1142
- );
1143
- await store.upsertLesson(
1144
- withLessonDefaults(
1145
- {
1146
- id: "lesson-miss",
1147
- scope: { workspaceId: ws("srch"), agentId: "a1" },
1148
- kind: "procedure",
1149
- text: "Prefer HTTPS",
1150
- confidence: 0.9,
1151
- occurrences: 1,
1152
- distinctSessionIds: ["s"],
1153
- evidence: [],
1154
- createdAt: "2026-08-15T00:00:00.000Z",
1155
- updatedAt: "2026-08-15T00:00:00.000Z",
1156
- },
1157
- "2026-08-15T00:00:00.000Z",
1158
- ),
1159
- );
1160
- await store.upsertCandidate(
1161
- withCandidateDefaults(
1162
- {
1163
- sessionId: "sess-search",
1164
- workspaceId: ws("srch"),
1165
- agentId: "a1",
1166
- excerpt: "Always use pnpm-lock.yaml",
1167
- tags: ["untagged"],
1168
- },
1169
- "2026-08-15T00:00:00.000Z",
1170
- ),
1171
- );
1172
- const hits = await store.searchLessons(
1173
- "pnpm-lock",
1174
- { workspaceId: ws("srch"), agentId: "a1" },
1175
- 5,
1176
- );
1177
- expect(hits[0]?.lesson.id).toBe("lesson-hit");
1178
- expect(hits[0]?.score).toBeGreaterThan(0);
1179
- expect(hits.map((h) => h.lesson.id)).not.toContain("lesson-miss");
1180
- expect(hits.map((h) => h.lesson.id)).not.toContain("sess-search");
1181
-
1182
- if (backend.name === "sqlite") {
1183
- const names = shared.db.prepare("SELECT name FROM sqlite_master").all() as Array<{
1184
- name: string;
1185
- }>;
1186
- expect(names.map((row) => row.name)).toContain("learning_lessons_fts");
1187
- } else if (backend.name === "postgres") {
1188
- if (backend.label.includes("live") && sharedPool) {
1189
- const cols = await sharedPool.query(
1190
- `SELECT column_name FROM information_schema.columns
1191
- WHERE table_schema = $1 AND table_name = 'learning_records' AND column_name = 'search_tsv'`,
1192
- [TEST_SCHEMA],
1193
- );
1194
- expect(cols.rows.length).toBeGreaterThan(0);
1195
- const idx = await sharedPool.query(
1196
- `SELECT indexname FROM pg_indexes
1197
- WHERE schemaname = $1 AND tablename = 'learning_records' AND indexdef ILIKE '%gin%'`,
1198
- [TEST_SCHEMA],
1199
- );
1200
- expect(idx.rows.length).toBeGreaterThan(0);
1201
- } else {
1202
- const sql = createLearningSchemaSql("postgres", { schema: TEST_SCHEMA }).join("\n");
1203
- expect(sql).toMatch(/search_tsv/i);
1204
- expect(sql).toMatch(/\bGIN\b/i);
1205
- }
1206
- } else {
1207
- expect(store.describeBackend().capability).toBe("reduced");
1208
- }
1209
- }
1210
-
1211
- async function assertFindProposalRejectsCrossScopeAmbiguity(make: MakeStore) {
1212
- // A proposal id is hash(lessonId + skillName) and a lesson id is hash(text) -- neither carries
1213
- // the scope. Two workspaces that learn the same text and propose the same skill therefore mint
1214
- // the SAME id, which the (workspace, agent, type, id) key permits. The cross-scope lookup then
1215
- // has no correct answer, so it must refuse rather than hand the gateway an arbitrary scope and
1216
- // apply one owner's decision to the other's proposal.
1217
- const shared: SkillProposal = {
1218
- id: "proposal-collides",
1219
- lessonIds: ["lesson-same-text"],
1220
- status: "pending",
1221
- skillName: "same-skill",
1222
- title: "Collides",
1223
- body: "# Collides",
1224
- scannerFindings: [],
1225
- createdAt: "2026-05-24T20:00:00.000Z",
1226
- updatedAt: "2026-05-24T20:00:00.000Z",
1227
- };
1228
- const left = await make({ agentId: "a1", workspaceId: ws("ambiguous-left") });
1229
- const right = await make({ agentId: "a2", workspaceId: ws("ambiguous-right") });
1230
- // upsertProposal, not insertProposalIfAbsent: the duplicate guard is scope-filtered, and the
1231
- // in-memory Postgres double does not honour that filter, so it would refuse the second write and
1232
- // never build the two-scope state this asserts on. The collision is what is under test here, not
1233
- // how it is created.
1234
- await left.upsertProposal(shared);
1235
- await right.upsertProposal(shared);
1236
-
1237
- const crossScope = await make({});
1238
- await expect(
1239
- crossScope.findProposalWithScope("proposal-collides"),
1240
- "an id present in two scopes must be refused, not resolved to an arbitrary one",
1241
- ).rejects.toThrow(/exists in 2 scopes/);
1242
- }
1243
-
1244
- async function assertListProposalsWithScopeKeepsCrossScopeSameId(make: MakeStore) {
1245
- const shared: SkillProposal = {
1246
- id: "proposal-collides-list",
1247
- lessonIds: ["lesson-same-text"],
1248
- status: "pending",
1249
- skillName: "same-skill",
1250
- title: "Collides",
1251
- body: "# Collides",
1252
- scannerFindings: [],
1253
- createdAt: "2026-05-24T20:00:00.000Z",
1254
- updatedAt: "2026-05-24T20:00:00.000Z",
1255
- };
1256
- const left = await make({ agentId: "a1", workspaceId: ws("list-left") });
1257
- const right = await make({ agentId: "a2", workspaceId: ws("list-right") });
1258
- // upsertProposal, not insertProposalIfAbsent — same reason as the ambiguity assert above
1259
- // (:1138-1141): the duplicate guard is scope-filtered and the in-memory postgres double does
1260
- // not honour that filter, so it would refuse the second write.
1261
- await left.upsertProposal(shared);
1262
- await right.upsertProposal(shared);
1263
-
1264
- const crossScope = await make({});
1265
- const matches = (await crossScope.listProposalsWithScope()).filter(
1266
- (entry) => entry.proposal.id === shared.id,
1267
- );
1268
- expect(
1269
- matches,
1270
- "both scopes' rows must be listed — fs-jsonl used to collapse them by recordId (finding 17)",
1271
- ).toHaveLength(2);
1272
- expect(matches.map((entry) => entry.scope)).toEqual(
1273
- expect.arrayContaining([
1274
- { agentId: "a1", workspaceId: ws("list-left") },
1275
- { agentId: "a2", workspaceId: ws("list-right") },
1276
- ]),
1277
- );
1278
- }
1279
-
1280
- async function assertAlreadyDecidedDecisionRejectsWithCode(make: MakeStore) {
1281
- const store = await make({ agentId: "a1", workspaceId: ws("decided-code") });
1282
- const proposal: SkillProposal = {
1283
- id: "proposal-decided-code",
1284
- lessonIds: ["lesson-x"],
1285
- status: "pending",
1286
- skillName: "decided-code",
1287
- title: "Decided",
1288
- body: "# Decided",
1289
- scannerFindings: [],
1290
- createdAt: "2026-05-24T20:00:00.000Z",
1291
- updatedAt: "2026-05-24T20:00:00.000Z",
1292
- };
1293
- await store.upsertProposal(proposal);
1294
- await store.recordProposalDecision({
1295
- proposalId: proposal.id,
1296
- status: "applied",
1297
- actor: "owner",
1298
- now: "2026-05-24T21:00:00.000Z",
1299
- });
1300
- await expect(
1301
- store.recordProposalDecision({
1302
- proposalId: proposal.id,
1303
- status: "rejected",
1304
- actor: "owner",
1305
- now: "2026-05-24T22:00:00.000Z",
1306
- }),
1307
- "an already-decided refusal must carry a LEARNING_* code on every backend leg",
1308
- ).rejects.toMatchObject({ code: expect.stringMatching(/^LEARNING_/) });
1309
- }
1310
-
1311
- async function assertInvalidateLesson(make: MakeStore) {
1312
- const scope = { agentId: "a1", workspaceId: ws("invalidate") };
1313
- const store = await make(scope);
1314
- const now = "2026-05-24T17:00:00.000Z";
1315
- const lesson = withLessonDefaults(
1316
- {
1317
- id: "lesson-invalidate",
1318
- scope,
1319
- kind: "procedure" as const,
1320
- text: "Use pnpm for installs.",
1321
- confidence: 0.8,
1322
- occurrences: 1,
1323
- distinctSessionIds: ["sess-1"],
1324
- evidence: [],
1325
- createdAt: now,
1326
- updatedAt: now,
1327
- },
1328
- now,
1329
- );
1330
- await store.upsertLesson(lesson);
1331
- const later = "2026-05-24T18:00:00.000Z";
1332
- const invalidated = await store.invalidateLesson({
1333
- id: "lesson-invalidate",
1334
- now: later,
1335
- reason: "contradicted",
1336
- sessionId: "sess-9",
1337
- });
1338
- expect(invalidated).toMatchObject({
1339
- id: "lesson-invalidate",
1340
- text: "Use pnpm for installs.",
1341
- invalidAt: later,
1342
- invalidatedBySessionId: "sess-9",
1343
- invalidationReason: "contradicted",
1344
- });
1345
- // The field round-trips through the store, no successor is minted, and it is inactive.
1346
- expect(await store.getLesson("lesson-invalidate")).toMatchObject({
1347
- invalidAt: later,
1348
- invalidatedBySessionId: "sess-9",
1349
- });
1350
- expect(await store.listLessons()).toHaveLength(1);
1351
- expect(await store.listActiveLessons(later)).toEqual([]);
1352
- // Idempotent: a second invalidation keeps the first invalidAt.
1353
- const again = await store.invalidateLesson({
1354
- id: "lesson-invalidate",
1355
- now: "2026-05-25T00:00:00.000Z",
1356
- });
1357
- expect(again.invalidAt).toBe(later);
1358
- await expect(store.invalidateLesson({ id: "lesson-missing", now: later })).rejects.toThrow(
1359
- /Lesson not found/,
1360
- );
1361
- }
1362
-
1363
- // VAL-PROD-022: invalidateLesson leaves a lesson retired with no successor. The public
1364
- // (non-exact) supersede must refuse it and write nothing: no restamped invalidAt, no active
1365
- // successor for a retired lesson.
1366
- async function assertSupersedeRefusesInvalidatedWithoutSuccessor(
1367
- make: MakeStore,
1368
- backend: Backend,
1369
- ) {
1370
- const scope = { agentId: "a1", workspaceId: ws("supersede-invalidated") };
1371
- const store = await make(scope);
1372
- const created = "2026-08-01T00:00:00.000Z";
1373
- const invalidatedAt = "2026-08-02T00:00:00.000Z";
1374
- await store.upsertLesson(
1375
- withLessonDefaults(
1376
- {
1377
- id: "lesson-retired",
1378
- scope,
1379
- kind: "correction",
1380
- text: "retired text",
1381
- confidence: 0.9,
1382
- occurrences: 1,
1383
- distinctSessionIds: ["s1"],
1384
- evidence: [],
1385
- createdAt: created,
1386
- updatedAt: created,
1387
- },
1388
- created,
1389
- ),
1390
- );
1391
- await store.invalidateLesson({
1392
- id: "lesson-retired",
1393
- now: invalidatedAt,
1394
- reason: "contradicted",
1395
- sessionId: "sess-inv",
1396
- });
1397
- const before = await backend.snapshot();
1398
- const later = "2026-08-03T00:00:00.000Z";
1399
- const error = await store
1400
- .supersedeLesson({
1401
- predecessorId: "lesson-retired",
1402
- successor: withLessonDefaults(
1403
- {
1404
- id: "lesson-resurrected",
1405
- scope,
1406
- kind: "correction",
1407
- text: "resurrected text",
1408
- confidence: 0.9,
1409
- occurrences: 1,
1410
- distinctSessionIds: ["s1"],
1411
- evidence: [],
1412
- createdAt: later,
1413
- updatedAt: later,
1414
- },
1415
- later,
1416
- ),
1417
- now: later,
1418
- })
1419
- .then(
1420
- () => undefined,
1421
- (err: unknown) => err,
1422
- );
1423
- expect(error).toBeInstanceOf(Error);
1424
- expect(isLearningDomainError(error)).toBe(true);
1425
- expect((error as Error).message).toMatch(/invalidated and has no successor/);
1426
- expect(await backend.snapshot()).toBe(before);
1427
- expect(await store.getLesson("lesson-retired")).toMatchObject({
1428
- invalidAt: invalidatedAt,
1429
- invalidatedBySessionId: "sess-inv",
1430
- invalidationReason: "contradicted",
1431
- });
1432
- expect(await store.getLesson("lesson-resurrected")).toBeUndefined();
1433
- const all = await store.listLessons();
1434
- expect(all.filter((lesson) => lesson.supersedesId === "lesson-retired")).toEqual([]);
1435
- expect(
1436
- (await store.listActiveLessons(later)).filter((l) => l.supersedesId === "lesson-retired"),
1437
- ).toEqual([]);
1438
- }
1439
-
1440
- async function assertDescribeBackend(make: MakeStore, expected: Backend["name"]) {
1441
- const store = await make({ agentId: "a9", workspaceId: ws("describe") });
1442
- const desc = store.describeBackend();
1443
-
1444
- expect(desc.backend, `${expected} store must report its own backend name`).toBe(expected);
1445
- expect(desc.capability, `${expected} store must report its documented capability`).toBe(
1446
- EXPECTED_CAPABILITY[expected],
1447
- );
1448
- }
1449
-
1450
97
  // --- Backend tests ---
1451
98
 
1452
99
  for (const backend of BACKENDS) {
1453
- const d = backend.available ? describe : describe.skip;
1454
- d(
1455
- `LessonStore conformance — ${backend.label}${backend.available ? "" : ` (SKIPPED: ${backend.skipReason})`}`,
1456
- () => {
1457
- it("round-trips a lesson with every lifecycle field", async () => {
1458
- await assertRoundTripsLesson(backend.make);
1459
- });
1460
-
1461
- it("upsert is idempotent by id within a scope", async () => {
1462
- await assertUpsertIsIdempotent(backend.make);
1463
- });
1464
-
1465
- it("scopes reads — a lesson in w1/a1 is invisible to w2/a2", async () => {
1466
- await assertScopeIsolation(backend.make);
1467
- });
1468
-
1469
- it("isolates on workspace and on agent independently", async () => {
1470
- await assertScopeIsolationPerDimension(backend.make);
1471
- });
1472
-
1473
- it("an empty scope reads across all scopes", async () => {
1474
- await assertEmptyScopeReadsAcrossScopes(backend.make);
1475
- });
1476
-
1477
- it("supersedeLesson retains the predecessor with invalid_at and links the successor", async () => {
1478
- await assertSupersedeRetainsPredecessor(backend.make);
1479
- });
1480
-
1481
- it("updating an earlier lesson does not reorder the list", async () => {
1482
- await assertUpdateDoesNotReorder(backend.make);
1483
- });
1484
-
1485
- it("supersedeLesson refuses a successor whose id equals the predecessor id", async () => {
1486
- await assertSupersedeRejectsSelfReference(backend.make);
1487
- });
1488
-
1489
- it("invalidateLesson ends a lesson without a successor and records invalidatedBySessionId", async () => {
1490
- await assertInvalidateLesson(backend.make);
1491
- });
1492
-
1493
- it("listActiveLessons excludes invalidated records; getLesson still returns them", async () => {
1494
- await assertActiveListExcludesInvalidated(backend.make);
1495
- });
1496
-
1497
- it("getLessonChain walks the chain newest-first and rejects a cycle", async () => {
1498
- await assertChainWalksNewestFirstAndRejectsCycle(backend.make);
1499
- });
1500
-
1501
- it("insertLessonIfAbsent never overwrites an existing row", async () => {
1502
- await assertInsertLessonIfAbsentNeverOverwrites(backend.make);
1503
- });
1504
-
1505
- it("mergeLesson accumulates occurrences and evidence", async () => {
1506
- await assertMergeAccumulatesOccurrencesAndEvidence(backend.make);
1507
- });
1508
-
1509
- it("proposal + audit round-trip", async () => {
1510
- await assertProposalAndAuditRoundTrip(backend.make);
1511
- });
1512
-
1513
- it("findProposalWithScope returns a proposal's own scope even when queried from an empty scope", async () => {
1514
- await assertFindProposalWithScope(backend.make);
1515
- });
1516
-
1517
- it("supersedeLesson REFUSES a successor id that already belongs to another lesson", async () => {
1518
- await assertSupersedeRejectsOccupiedSuccessorId(backend.make);
1519
- });
1520
-
1521
- it("supersedeLesson refuses an invalidated lesson with no successor and writes nothing (VAL-PROD-022)", async () => {
1522
- await assertSupersedeRefusesInvalidatedWithoutSuccessor(backend.make, backend);
1523
- });
1524
-
1525
- it("re-superseding an invalidated lesson returns the existing successor", async () => {
1526
- await assertIdempotentResupersede(backend.make);
1527
- });
1528
-
1529
- it("findProposalWithScope REFUSES an id that exists in two scopes", async () => {
1530
- await assertFindProposalRejectsCrossScopeAmbiguity(backend.make);
1531
- });
1532
-
1533
- it("listProposalsWithScope keeps both scopes' rows for a cross-scope same-id proposal", async () => {
1534
- await assertListProposalsWithScopeKeepsCrossScopeSameId(backend.make);
1535
- });
1536
-
1537
- it("recordProposalDecision on an already-decided proposal rejects with a LEARNING_* code", async () => {
1538
- await assertAlreadyDecidedDecisionRejectsWithCode(backend.make);
1539
- });
1540
-
1541
- it("describeBackend reports the expected backend and capability", async () => {
1542
- await assertDescribeBackend(backend.make, backend.name);
1543
- });
1544
-
1545
- it("round-trips a candidate and keeps it out of listLessons", async () => {
1546
- await assertCandidateRoundTrip(backend.make);
1547
- });
1548
-
1549
- it("batches candidate lookups by session id set", async () => {
1550
- await assertGetCandidatesBySessionIdsRoundTrip(backend.make);
1551
- });
1552
-
1553
- it("searchLessons ranks the distinctive token and skips candidates (TS12)", async () => {
1554
- await assertSearchLessons(backend.make, backend);
1555
- });
1556
- },
1557
- );
100
+ registerLessonStoreConformance(backend);
1558
101
  }
1559
102
 
1560
- // --- Skip reporting test (TS3) ---
1561
-
1562
- it("reports which backends were skipped (TS3)", () => {
1563
- const skipped = BACKENDS.filter((b) => !b.available).map((b) => `${b.label}: ${b.skipReason}`);
1564
- console.warn(
1565
- skipped.length
1566
- ? `conformance skipped — ${skipped.join(", ")}`
1567
- : `conformance: all ${BACKENDS.length} legs ran`,
1568
- );
1569
- expect(BACKENDS.filter((b) => b.available).length).toBeGreaterThanOrEqual(2);
1570
- });
1571
-
1572
- // The in-memory double must never be mistaken for coverage of a real server. This asserts the
1573
- // report above still names the live leg as skipped when no DSN is configured, so a green run on a
1574
- // database-less host cannot read as "postgres verified".
1575
- it("does not let the in-memory double mask an unrun live-postgres leg (TS3)", () => {
1576
- const live = BACKENDS.find((b) => b.label === "postgres (live server)");
1577
- expect(live).toBeDefined();
1578
- expect(live?.available).toBe(Boolean(process.env.OPENCLAW_DB_URL));
1579
-
1580
- const double = BACKENDS.find((b) => b.label === "postgres (in-memory double)");
1581
- expect(double?.available).toBe(true);
1582
- expect(double?.name).toBe("postgres");
1583
- });
1584
-
1585
103
  // --- Byte-identity test (TS1) ---
1586
104
 
1587
105
  it("fs-jsonl never rewrites an existing log on read (TS1 byte-identity)", async () => {